Skip to content

Commit c365278

Browse files
committed
refactor(@schematics/angular): add an internal Tailwind CSS schematic
Adds a new schematic to set up Tailwind CSS in a new project. This is currently unused within the Angular CLI and will be further integrated in later changes. The schematic: - Adds the necessary dependencies ('tailwindcss', '@tailwindcss/postcss', 'postcss'). - Creates a '.postcssrc.json' file with the correct plugin configuration. - Injects the '@import "tailwindcss";' into the global stylesheet.
1 parent 3a637c0 commit c365278

File tree

6 files changed

+181
-0
lines changed

6 files changed

+181
-0
lines changed

packages/schematics/angular/collection.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@
136136
"factory": "./ai-config",
137137
"schema": "./ai-config/schema.json",
138138
"description": "Generates an AI tool configuration file."
139+
},
140+
"tailwind": {
141+
"factory": "./tailwind",
142+
"schema": "./tailwind/schema.json",
143+
"hidden": true,
144+
"private": true,
145+
"description": "[INTERNAL] Adds tailwind to a project. Intended for use for ng new/add."
139146
}
140147
}
141148
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"plugins": {
3+
"@tailwindcss/postcss": {}
4+
}
5+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
type Rule,
11+
SchematicsException,
12+
apply,
13+
applyTemplates,
14+
chain,
15+
mergeWith,
16+
move,
17+
strings,
18+
url,
19+
} from '@angular-devkit/schematics';
20+
import { DependencyType, ExistingBehavior, addDependency } from '../utility';
21+
import { latestVersions } from '../utility/latest-versions';
22+
import { getWorkspace } from '../utility/workspace';
23+
import { Schema } from './schema';
24+
25+
const TAILWIND_DEPENDENCIES = ['tailwindcss', '@tailwindcss/postcss', 'postcss'];
26+
27+
function addTailwindImport(stylesheetPath: string): Rule {
28+
return (tree) => {
29+
let stylesheetText = '';
30+
31+
if (tree.exists(stylesheetPath)) {
32+
stylesheetText = tree.readText(stylesheetPath);
33+
stylesheetText += '\n';
34+
}
35+
36+
stylesheetText += '@import "tailwindcss";\n';
37+
38+
tree.overwrite(stylesheetPath, stylesheetText);
39+
};
40+
}
41+
42+
export default function (options: Schema): Rule {
43+
return async (tree) => {
44+
const workspace = await getWorkspace(tree);
45+
const project = workspace.projects.get(options.project);
46+
47+
if (!project) {
48+
throw new SchematicsException(`Project "${options.project}" does not exist.`);
49+
}
50+
51+
const buildTarget = project.targets.get('build');
52+
53+
if (!buildTarget) {
54+
throw new SchematicsException(`Project "${options.project}" does not have a build target.`);
55+
}
56+
57+
const styles = buildTarget.options?.['styles'] as string[] | undefined;
58+
59+
if (!styles || styles.length === 0) {
60+
throw new SchematicsException(
61+
`Project "${options.project}" does not have any global styles.`,
62+
);
63+
}
64+
65+
const stylesheetPath = styles[0];
66+
67+
const templateSource = apply(url('./files'), [
68+
applyTemplates({
69+
...strings,
70+
...options,
71+
}),
72+
move(project.root),
73+
]);
74+
75+
return chain([
76+
addTailwindImport(stylesheetPath),
77+
mergeWith(templateSource),
78+
...TAILWIND_DEPENDENCIES.map((name) =>
79+
addDependency(name, latestVersions[name], {
80+
type: DependencyType.Dev,
81+
existing: ExistingBehavior.Skip,
82+
}),
83+
),
84+
]);
85+
};
86+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
10+
import { Schema as ApplicationOptions, Style } from '../application/schema';
11+
import { Schema as WorkspaceOptions } from '../workspace/schema';
12+
13+
describe('Tailwind Schematic', () => {
14+
const schematicRunner = new SchematicTestRunner(
15+
'@schematics/angular',
16+
require.resolve('../collection.json'),
17+
);
18+
19+
const workspaceOptions: WorkspaceOptions = {
20+
name: 'workspace',
21+
newProjectRoot: 'projects',
22+
version: '6.0.0',
23+
};
24+
25+
const appOptions: ApplicationOptions = {
26+
name: 'bar',
27+
inlineStyle: false,
28+
inlineTemplate: false,
29+
routing: false,
30+
style: Style.Css,
31+
skipTests: false,
32+
skipPackageJson: false,
33+
};
34+
35+
let appTree: UnitTestTree;
36+
37+
beforeEach(async () => {
38+
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
39+
appTree = await schematicRunner.runSchematic('application', appOptions, appTree);
40+
});
41+
42+
it('should add tailwind dependencies', async () => {
43+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
44+
const packageJson = JSON.parse(tree.readContent('/package.json'));
45+
expect(packageJson.devDependencies['tailwindcss']).toBeDefined();
46+
expect(packageJson.devDependencies['postcss']).toBeDefined();
47+
expect(packageJson.devDependencies['@tailwindcss/postcss']).toBeDefined();
48+
});
49+
50+
it('should create a .postcssrc.json file in the project root', async () => {
51+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
52+
expect(tree.exists('/projects/bar/.postcssrc.json')).toBe(true);
53+
});
54+
55+
it('should configure tailwindcss plugin in .postcssrc.json', async () => {
56+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
57+
const postCssConfig = JSON.parse(tree.readContent('/projects/bar/.postcssrc.json'));
58+
expect(postCssConfig.plugins['@tailwindcss/postcss']).toBeDefined();
59+
});
60+
61+
it('should add tailwind imports to styles.css', async () => {
62+
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
63+
const stylesContent = tree.readContent('/projects/bar/src/styles.css');
64+
expect(stylesContent).toContain('@import "tailwindcss";');
65+
});
66+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "Tailwind CSS Schematic",
4+
"type": "object",
5+
"properties": {
6+
"project": {
7+
"type": "string",
8+
"description": "The name of the project.",
9+
"$default": {
10+
"$source": "projectName"
11+
}
12+
}
13+
},
14+
"required": ["project"]
15+
}

packages/schematics/angular/utility/latest-versions/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"postcss": "^8.5.3",
2020
"protractor": "~7.0.0",
2121
"rxjs": "~7.8.0",
22+
"tailwindcss": "^4.1.12",
23+
"@tailwindcss/postcss": "^4.1.12",
2224
"tslib": "^2.3.0",
2325
"ts-node": "~10.9.0",
2426
"typescript": "~5.9.2",

0 commit comments

Comments
 (0)