Skip to content

Commit 86c3324

Browse files
cipchkclaude
andcommitted
feat(schematics): add tailwindcss option to ng-add prompt
- Add x-prompt for tailwindcss in ng-add schema (default: N) - Implement addTailwindcss() rule: deps, .postcssrc.json, tailwind.css, styles.less layering, angular.json, .vscode/extensions - Add 8 test cases covering true/false scenarios - Fix ESLint prefer-template issues Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 367f216 commit 86c3324

6 files changed

Lines changed: 158 additions & 0 deletions

File tree

schematics/application/index.spec.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,92 @@ describe('NgAlainSchematic: application', () => {
127127
});
128128
});
129129

130+
describe('#tailwindcss', () => {
131+
describe('with false', () => {
132+
beforeEach(async () => ({ tree } = await createAlainApp()));
133+
it(`should not add tailwind related files`, () => {
134+
expect(tree.exists('.postcssrc.json')).toBe(false);
135+
expect(tree.exists(`/projects/${APPNAME}/src/tailwind.css`)).toBe(false);
136+
const angularJson = JSON.parse(tree.readContent('angular.json'));
137+
const styles = angularJson.projects[APPNAME].architect.build.options.styles;
138+
expect(styles.some((s: string) => s.includes('tailwind.css'))).toBe(false);
139+
const packageJson = JSON.parse(tree.readContent('package.json'));
140+
expect(packageJson.devDependencies['tailwindcss']).toBeUndefined();
141+
});
142+
});
143+
describe('with true', () => {
144+
beforeEach(async () => {
145+
const baseRunner = createNgRunner();
146+
const workspaceTree = await baseRunner.runSchematic('workspace', {
147+
name: 'workspace',
148+
newProjectRoot: 'projects',
149+
version: '6.0.0'
150+
});
151+
const appTree = await baseRunner.runSchematic(
152+
'application',
153+
{
154+
name: APPNAME,
155+
inlineStyle: false,
156+
inlineTemplate: false,
157+
routing: false,
158+
style: 'css',
159+
skipTests: false,
160+
skipPackageJson: false
161+
},
162+
workspaceTree
163+
);
164+
const alainRunner = createAlainRunner();
165+
tree = await alainRunner.runSchematic(
166+
'ng-add',
167+
{
168+
skipPackageJson: false,
169+
tailwindcss: true
170+
},
171+
appTree
172+
);
173+
});
174+
175+
it(`should add tailwindcss as devDependencies`, () => {
176+
const packageJson = JSON.parse(tree.readContent('package.json'));
177+
expect(packageJson.devDependencies['tailwindcss']).toBeDefined();
178+
expect(packageJson.devDependencies['@tailwindcss/postcss']).toBeDefined();
179+
expect(packageJson.devDependencies['postcss']).toBeDefined();
180+
});
181+
182+
it(`should generate .postcssrc.json`, () => {
183+
expect(tree.exists('.postcssrc.json')).toBe(true);
184+
const postcssrc = JSON.parse(tree.readContent('.postcssrc.json'));
185+
expect(postcssrc.plugins['@tailwindcss/postcss']).toEqual({});
186+
});
187+
188+
it(`should generate src/tailwind.css`, () => {
189+
expect(tree.exists(`/projects/${APPNAME}/src/tailwind.css`)).toBe(true);
190+
const content = tree.readContent(`/projects/${APPNAME}/src/tailwind.css`);
191+
expect(content).toContain(`@layer theme, base, ng-alain, utilities;`);
192+
expect(content).toContain(`@import 'tailwindcss'`);
193+
});
194+
195+
it(`should handle styles.less gracefully when file does not exist (no crash)`, () => {
196+
// removeOrginalFiles deletes styles.less before addTailwindcss
197+
// In test env (--style css), styles.less never existed
198+
// The function should handle missing styles.less gracefully
199+
expect(tree.exists(`/projects/${APPNAME}/src/styles.less`)).toBe(false);
200+
});
201+
202+
it(`should add src/tailwind.css to angular.json styles`, () => {
203+
const angularJson = JSON.parse(tree.readContent('angular.json'));
204+
const styles = angularJson.projects[APPNAME].architect.build.options.styles;
205+
expect(styles.some((s: string) => s.includes('tailwind.css'))).toBe(true);
206+
});
207+
208+
it(`should update .vscode/extensions.json with tailwindcss recommendation`, () => {
209+
expect(tree.exists('.vscode/extensions.json')).toBe(true);
210+
const ext = JSON.parse(tree.readContent('.vscode/extensions.json'));
211+
expect(ext.recommendations).toContain('bradlc.vscode-tailwindcss');
212+
});
213+
});
214+
});
215+
130216
describe('#multiple-projects', () => {
131217
let runner: SchematicTestRunner;
132218
let tree: UnitTestTree;

schematics/application/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,64 @@ function fixNgAlainJson(): Rule {
385385
};
386386
}
387387

388+
function addTailwindcss(options: ApplicationOptions): Rule {
389+
if (!options.tailwindcss) return noop();
390+
391+
const sourceRoot = project.sourceRoot!;
392+
const prefix = mulitProject ? `projects/${projectName}/` : '';
393+
394+
return chain([
395+
(tree: Tree) => {
396+
// Add devDependencies
397+
addPackage(
398+
tree,
399+
[
400+
'tailwindcss@DEP-0.0.0-PLACEHOLDER',
401+
'@tailwindcss/postcss@DEP-0.0.0-PLACEHOLDER',
402+
'postcss@DEP-0.0.0-PLACEHOLDER'
403+
],
404+
'devDependencies'
405+
);
406+
407+
// Create .postcssrc.json
408+
tree.create('.postcssrc.json', `${JSON.stringify({ plugins: { '@tailwindcss/postcss': {} } }, null, 2)}\n`);
409+
410+
// Create src/tailwind.css
411+
tree.create(`${sourceRoot}/tailwind.css`, `@layer theme, base, ng-alain, utilities;\n\n@import 'tailwindcss';\n`);
412+
413+
// Wrap styles.less in @layer ng-alain
414+
const stylesLessPath = `${sourceRoot}/styles.less`;
415+
if (tree.exists(stylesLessPath)) {
416+
const content = tree.read(stylesLessPath)!.toString('utf8');
417+
const wrappedContent = `/* stylelint-disable no-invalid-position-at-import-rule */\n@layer ng-alain {\n${content}}\n`;
418+
tree.overwrite(stylesLessPath, wrappedContent);
419+
}
420+
421+
// Create/update .vscode/extensions.json with tailwindcss recommendation
422+
const extPath = '.vscode/extensions.json';
423+
const extId = 'bradlc.vscode-tailwindcss';
424+
if (tree.exists(extPath)) {
425+
const json = readJSON(tree, extPath);
426+
if (!json.recommendations) json.recommendations = [];
427+
if (!json.recommendations.includes(extId)) json.recommendations.push(extId);
428+
writeJSON(tree, extPath, json);
429+
} else {
430+
tree.create(extPath, `${JSON.stringify({ recommendations: [extId] }, null, 2)}\n`);
431+
}
432+
433+
return tree;
434+
},
435+
// Add src/tailwind.css to angular.json styles
436+
addAssetsToTarget(
437+
[{ type: 'style', value: `${prefix}src/tailwind.css` }],
438+
'add',
439+
[BUILD_TARGET_BUILD],
440+
projectName,
441+
false
442+
)
443+
]);
444+
}
445+
388446
export default function (options: ApplicationOptions): Rule {
389447
return async (tree: Tree, context: SchematicContext) => {
390448
const res = await getProject(tree, options.project);
@@ -411,6 +469,7 @@ export default function (options: ApplicationOptions): Rule {
411469
addFilesToRoot(options),
412470
forceLess(),
413471
addStyle(),
472+
addTailwindcss(options),
414473
fixLang(options),
415474
fixAngularJson(),
416475
fixBrowserBuilderBudgets(),

schematics/application/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "NG-ALAIN Shell Options Schema",
55
"type": "object",
66
"properties": {
7+
"tailwindcss": {
8+
"type": "boolean",
9+
"default": false,
10+
"description": "Generate Tailwind CSS plugin"
11+
},
712
"codeStyle": {
813
"type": "boolean",
914
"default": true,

schematics/application/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface Schema {
33
mock?: boolean;
44
reuseTab?: boolean;
55
i18n?: boolean;
6+
tailwindcss?: boolean;
67
codeStyle?: boolean;
78
project?: string;
89
defaultLanguage?: string;

schematics/ng-add/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
]
5757
}
5858
},
59+
"tailwindcss": {
60+
"type": "boolean",
61+
"default": false,
62+
"description": "Generate Tailwind CSS plugin",
63+
"x-prompt": "Would you like to add Tailwind CSS? (default: N)"
64+
},
5965
"codeStyle": {
6066
"type": "boolean",
6167
"default": true,

schematics/ng-add/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export interface Schema {
55
reuseTab?: boolean;
66
defaultLanguage?: string;
77
i18n?: boolean;
8+
tailwindcss?: boolean;
89
codeStyle?: boolean;
910
}

0 commit comments

Comments
 (0)