Skip to content

Commit eeca71a

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 8727032 commit eeca71a

File tree

6 files changed

+169
-0
lines changed

6 files changed

+169
-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: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 { createProjectSchematic } from '../utility/project';
23+
24+
const TAILWIND_DEPENDENCIES = ['tailwindcss', '@tailwindcss/postcss', 'postcss'];
25+
26+
function addTailwindImport(stylesheetPath: string): Rule {
27+
return (tree) => {
28+
let stylesheetText = '';
29+
30+
if (tree.exists(stylesheetPath)) {
31+
stylesheetText = tree.readText(stylesheetPath);
32+
stylesheetText += '\n';
33+
}
34+
35+
stylesheetText += '@import "tailwindcss";\n';
36+
37+
tree.overwrite(stylesheetPath, stylesheetText);
38+
};
39+
}
40+
41+
export default createProjectSchematic((options, { project }) => {
42+
const buildTarget = project.targets.get('build');
43+
44+
if (!buildTarget) {
45+
throw new SchematicsException(`Project "${options.project}" does not have a build target.`);
46+
}
47+
48+
const styles = buildTarget.options?.['styles'] as string[] | undefined;
49+
50+
if (!styles || styles.length === 0) {
51+
throw new SchematicsException(`Project "${options.project}" does not have any global styles.`);
52+
}
53+
54+
const stylesheetPath = styles[0];
55+
56+
const templateSource = apply(url('./files'), [
57+
applyTemplates({
58+
...strings,
59+
...options,
60+
}),
61+
move(project.root),
62+
]);
63+
64+
return chain([
65+
addTailwindImport(stylesheetPath),
66+
mergeWith(templateSource),
67+
...TAILWIND_DEPENDENCIES.map((name) =>
68+
addDependency(name, latestVersions[name], {
69+
type: DependencyType.Dev,
70+
existing: ExistingBehavior.Skip,
71+
}),
72+
),
73+
]);
74+
});
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)