Skip to content

Commit 58acde9

Browse files
committed
refactor(@schematics/angular): add dynamic stylesheet injection to tailwind schematic
The tailwind schematic's stylesheet injection logic has been updated to support a wider range of project configurations. Previously, it assumed a `styles.css` file was always present, which could cause issues in projects using CSS preprocessors like Sass/SCSS. This change introduces the following logic: - Detects if a global CSS file already exists to add the Tailwind import. - Creates a new `tailwind.css` file if no global CSS file is found. - Updates the build configuration in `angular.json` to include the new `tailwind.css` file. - Updates the `styles` array in all build configurations (`production`, `development`, etc.) to ensure Tailwind is included in all builds. - Ensures the schematic is idempotent by checking for existing Tailwind imports before adding a new one. These changes make the schematic more robust and provide a better user experience for a wider variety of project setups.
1 parent 280f257 commit 58acde9

File tree

2 files changed

+143
-23
lines changed

2 files changed

+143
-23
lines changed

packages/schematics/angular/tailwind/index.ts

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,75 @@ import {
1717
strings,
1818
url,
1919
} from '@angular-devkit/schematics';
20-
import { DependencyType, ExistingBehavior, addDependency } from '../utility';
20+
import assert from 'node:assert';
21+
import { join } from 'node:path/posix';
22+
import {
23+
DependencyType,
24+
ExistingBehavior,
25+
ProjectDefinition,
26+
addDependency,
27+
updateWorkspace,
28+
} from '../utility';
2129
import { latestVersions } from '../utility/latest-versions';
2230
import { createProjectSchematic } from '../utility/project';
2331

2432
const TAILWIND_DEPENDENCIES = ['tailwindcss', '@tailwindcss/postcss', 'postcss'];
2533

26-
function addTailwindImport(stylesheetPath: string): Rule {
27-
return (tree) => {
28-
let stylesheetText = '';
34+
function addTailwindStyles(options: { project: string }, project: ProjectDefinition): Rule {
35+
return async (tree) => {
36+
const buildTarget = project.targets.get('build');
2937

30-
if (tree.exists(stylesheetPath)) {
31-
stylesheetText = tree.readText(stylesheetPath);
32-
stylesheetText += '\n';
38+
if (!buildTarget) {
39+
throw new SchematicsException(`Project "${options.project}" does not have a build target.`);
3340
}
3441

35-
stylesheetText += '@import "tailwindcss";\n';
42+
const styles = buildTarget.options?.['styles'] as (string | { input: string })[] | undefined;
3643

37-
tree.overwrite(stylesheetPath, stylesheetText);
38-
};
39-
}
40-
41-
export default createProjectSchematic((options, { project }) => {
42-
const buildTarget = project.targets.get('build');
44+
let stylesheetPath: string | undefined;
45+
if (styles) {
46+
stylesheetPath = styles
47+
.map((s) => (typeof s === 'string' ? s : s.input))
48+
.find((p) => p.endsWith('.css'));
49+
}
4350

44-
if (!buildTarget) {
45-
throw new SchematicsException(`Project "${options.project}" does not have a build target.`);
46-
}
51+
if (!stylesheetPath) {
52+
const newStylesheetPath = join(project.sourceRoot ?? 'src', 'tailwind.css');
53+
tree.create(newStylesheetPath, '@import "tailwindcss";\n');
4754

48-
const styles = buildTarget.options?.['styles'] as string[] | undefined;
55+
return updateWorkspace((workspace) => {
56+
const project = workspace.projects.get(options.project);
57+
if (project) {
58+
const buildTarget = project.targets.get('build');
59+
assert(buildTarget, 'Build target should still be present');
4960

50-
if (!styles || styles.length === 0) {
51-
throw new SchematicsException(`Project "${options.project}" does not have any global styles.`);
52-
}
61+
// Update main styles
62+
const buildOptions = buildTarget.options;
63+
assert(buildOptions, 'Build options should still be present');
64+
const existingStyles = (buildOptions['styles'] as (string | { input: string })[]) ?? [];
65+
buildOptions['styles'] = [newStylesheetPath, ...existingStyles];
5366

54-
const stylesheetPath = styles[0];
67+
// Update configuration styles
68+
if (buildTarget.configurations) {
69+
for (const config of Object.values(buildTarget.configurations)) {
70+
if (config && 'styles' in config) {
71+
const existingStyles = (config['styles'] as (string | { input: string })[]) ?? [];
72+
config['styles'] = [newStylesheetPath, ...existingStyles];
73+
}
74+
}
75+
}
76+
}
77+
});
78+
} else {
79+
let stylesheetContent = tree.readText(stylesheetPath);
80+
if (!stylesheetContent.includes('@import "tailwindcss";')) {
81+
stylesheetContent += '\n@import "tailwindcss";\n';
82+
tree.overwrite(stylesheetPath, stylesheetContent);
83+
}
84+
}
85+
};
86+
}
5587

88+
export default createProjectSchematic((options, { project }) => {
5689
const templateSource = apply(url('./files'), [
5790
applyTemplates({
5891
...strings,
@@ -62,7 +95,7 @@ export default createProjectSchematic((options, { project }) => {
6295
]);
6396

6497
return chain([
65-
addTailwindImport(stylesheetPath),
98+
addTailwindStyles(options, project),
6699
mergeWith(templateSource),
67100
...TAILWIND_DEPENDENCIES.map((name) =>
68101
addDependency(name, latestVersions[name], {

packages/schematics/angular/tailwind/index_spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,91 @@ describe('Tailwind Schematic', () => {
6363
const stylesContent = tree.readContent('/projects/bar/src/styles.css');
6464
expect(stylesContent).toContain('@import "tailwindcss";');
6565
});
66+
67+
it('should not add duplicate tailwind imports to styles.css', async () => {
68+
let tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
69+
const stylesContent = tree.readContent('/projects/bar/src/styles.css');
70+
expect(stylesContent.match(/@import "tailwindcss";/g)?.length).toBe(1);
71+
72+
tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, tree);
73+
const stylesContentAfter = tree.readContent('/projects/bar/src/styles.css');
74+
expect(stylesContentAfter.match(/@import "tailwindcss";/g)?.length).toBe(1);
75+
});
76+
77+
describe('with scss styles', () => {
78+
beforeEach(async () => {
79+
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
80+
appTree = await schematicRunner.runSchematic(
81+
'application',
82+
{ ...appOptions, style: Style.Scss },
83+
appTree,
84+
);
85+
});
86+
87+
it('should create a tailwind.css file', async () => {
88+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
89+
expect(tree.exists('/projects/bar/src/tailwind.css')).toBe(true);
90+
const stylesContent = tree.readContent('/projects/bar/src/tailwind.css');
91+
expect(stylesContent).toContain('@import "tailwindcss";');
92+
});
93+
94+
it('should add tailwind.css to angular.json', async () => {
95+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
96+
const angularJson = JSON.parse(tree.readContent('/angular.json'));
97+
const styles = angularJson.projects.bar.architect.build.options.styles;
98+
expect(styles).toEqual(['projects/bar/src/tailwind.css', 'projects/bar/src/styles.scss']);
99+
});
100+
101+
it('should not add tailwind imports to styles.scss', async () => {
102+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
103+
const stylesContent = tree.readContent('/projects/bar/src/styles.scss');
104+
expect(stylesContent).not.toContain('@import "tailwindcss";');
105+
});
106+
});
107+
108+
describe('with complex build configurations', () => {
109+
beforeEach(async () => {
110+
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
111+
appTree = await schematicRunner.runSchematic(
112+
'application',
113+
{ ...appOptions, style: Style.Scss },
114+
appTree,
115+
);
116+
117+
const angularJson = JSON.parse(appTree.readContent('/angular.json'));
118+
angularJson.projects.bar.architect.build.configurations = {
119+
...angularJson.projects.bar.architect.build.configurations,
120+
staging: {
121+
styles: [],
122+
},
123+
production: {
124+
styles: ['projects/bar/src/styles.prod.scss'],
125+
},
126+
development: {
127+
// No styles property
128+
},
129+
};
130+
appTree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2));
131+
});
132+
133+
it('should add tailwind.css to all configurations with styles', async () => {
134+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
135+
const angularJson = JSON.parse(tree.readContent('/angular.json'));
136+
const { configurations } = angularJson.projects.bar.architect.build;
137+
138+
expect(configurations.production.styles).toEqual([
139+
'projects/bar/src/tailwind.css',
140+
'projects/bar/src/styles.prod.scss',
141+
]);
142+
expect(configurations.staging.styles).toEqual(['projects/bar/src/tailwind.css']);
143+
});
144+
145+
it('should not modify configurations without a styles property', async () => {
146+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
147+
const angularJson = JSON.parse(tree.readContent('/angular.json'));
148+
const { configurations } = angularJson.projects.bar.architect.build;
149+
150+
expect(configurations.development.styles).toBeUndefined();
151+
});
152+
});
66153
});

0 commit comments

Comments
 (0)