Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/schematics/angular/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
strings,
url,
} from '@angular-devkit/schematics';
import { Schema as ComponentOptions } from '../component/schema';
import { Schema as ComponentOptions, Style as ComponentStyle } from '../component/schema';
import {
DependencyType,
ExistingBehavior,
Expand Down Expand Up @@ -59,6 +59,11 @@ function addTsProjectReference(...paths: string[]) {

export default function (options: ApplicationOptions): Rule {
return async (host: Tree) => {
const isTailwind = options.style === Style.Tailwind;
if (isTailwind) {
options.style = Style.Css;
}

const { appDir, appRootSelector, componentOptions, folderName, sourceDir } =
await getAppOptions(host, options);

Expand Down Expand Up @@ -135,6 +140,11 @@ export default function (options: ApplicationOptions): Rule {
})
: noop(),
options.skipPackageJson ? noop() : addDependenciesToPackageJson(options),
isTailwind
? schematic('tailwind', {
project: options.name,
})
: noop(),
]);
};
}
Expand Down Expand Up @@ -368,14 +378,14 @@ function getComponentOptions(options: ApplicationOptions): Partial<ComponentOpti
inlineStyle: options.inlineStyle,
inlineTemplate: options.inlineTemplate,
skipTests: options.skipTests,
style: options.style,
style: options.style as unknown as ComponentStyle,
viewEncapsulation: options.viewEncapsulation,
}
: {
inlineStyle: options.inlineStyle ?? true,
inlineTemplate: options.inlineTemplate ?? true,
skipTests: true,
style: options.style,
style: options.style as unknown as ComponentStyle,
viewEncapsulation: options.viewEncapsulation,
};

Expand Down
16 changes: 16 additions & 0 deletions packages/schematics/angular/application/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,4 +872,20 @@ describe('Application Schematic', () => {
expect(fileContent).not.toContain('provideZoneChangeDetection');
});
});

it('should call the tailwind schematic when style is tailwind', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = { ...defaultOptions, style: 'tailwind' as any };
const tree = await schematicRunner.runSchematic('application', options, workspaceTree);

expect(tree.exists('/projects/foo/.postcssrc.json')).toBe(true);

const packageJson = JSON.parse(tree.readContent('/package.json'));
expect(packageJson.devDependencies['tailwindcss']).toBeDefined();
expect(packageJson.devDependencies['postcss']).toBeDefined();
expect(packageJson.devDependencies['@tailwindcss/postcss']).toBeDefined();

const stylesContent = tree.readContent('/projects/foo/src/styles.css');
expect(stylesContent).toContain('@import "tailwindcss";');
});
});
8 changes: 6 additions & 2 deletions packages/schematics/angular/application/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,19 @@
"description": "The type of stylesheet files to be created for components in the application.",
"type": "string",
"default": "css",
"enum": ["css", "scss", "sass", "less"],
"enum": ["css", "scss", "sass", "less", "tailwind"],
"x-prompt": {
"message": "Which stylesheet format would you like to use?",
"message": "Which stylesheet system would you like to use?",
"type": "list",
"items": [
{
"value": "css",
"label": "CSS [ https://developer.mozilla.org/docs/Web/CSS ]"
},
{
"value": "tailwind",
"label": "Tailwind CSS [ https://tailwindcss.com ]"
},
{
"value": "scss",
"label": "Sass (SCSS) [ https://sass-lang.com/documentation/syntax#scss ]"
Expand Down
7 changes: 7 additions & 0 deletions packages/schematics/angular/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@
"factory": "./ai-config",
"schema": "./ai-config/schema.json",
"description": "Generates an AI tool configuration file."
},
"tailwind": {
"factory": "./tailwind",
"schema": "./tailwind/schema.json",
"hidden": true,
"private": true,
"description": "[INTERNAL] Adds tailwind to a project. Intended for use for ng new/add."
}
}
}
16 changes: 16 additions & 0 deletions packages/schematics/angular/ng-new/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,20 @@ describe('Ng New Schematic', () => {
expect(files).toContain('/bar/.gemini/GEMINI.md');
expect(files).toContain('/bar/.claude/CLAUDE.md');
});

it('should create a tailwind project when style is tailwind', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = { ...defaultOptions, style: 'tailwind' as any };
const tree = await schematicRunner.runSchematic('ng-new', options);

expect(tree.exists('/bar/.postcssrc.json')).toBe(true);

const packageJson = JSON.parse(tree.readContent('/bar/package.json'));
expect(packageJson.devDependencies['tailwindcss']).toBeDefined();
expect(packageJson.devDependencies['postcss']).toBeDefined();
expect(packageJson.devDependencies['@tailwindcss/postcss']).toBeDefined();

const stylesContent = tree.readContent('/bar/src/styles.css');
expect(stylesContent).toContain('@import "tailwindcss";');
});
});
2 changes: 1 addition & 1 deletion packages/schematics/angular/ng-new/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"style": {
"description": "The type of stylesheet files to be created for components in the initial project.",
"type": "string",
"enum": ["css", "scss", "sass", "less"],
"enum": ["css", "scss", "sass", "less", "tailwind"],
"x-user-analytics": "ep.ng_style"
},
"skipTests": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
74 changes: 74 additions & 0 deletions packages/schematics/angular/tailwind/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {
type Rule,
SchematicsException,
apply,
applyTemplates,
chain,
mergeWith,
move,
strings,
url,
} from '@angular-devkit/schematics';
import { DependencyType, ExistingBehavior, addDependency } from '../utility';
import { latestVersions } from '../utility/latest-versions';
import { createProjectSchematic } from '../utility/project';

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

function addTailwindImport(stylesheetPath: string): Rule {
return (tree) => {
let stylesheetText = '';

if (tree.exists(stylesheetPath)) {
stylesheetText = tree.readText(stylesheetPath);
stylesheetText += '\n';
}

stylesheetText += '@import "tailwindcss";\n';

tree.overwrite(stylesheetPath, stylesheetText);
};
}

export default createProjectSchematic((options, { project }) => {
const buildTarget = project.targets.get('build');

if (!buildTarget) {
throw new SchematicsException(`Project "${options.project}" does not have a build target.`);
}

const styles = buildTarget.options?.['styles'] as string[] | undefined;

if (!styles || styles.length === 0) {
throw new SchematicsException(`Project "${options.project}" does not have any global styles.`);
}

const stylesheetPath = styles[0];

const templateSource = apply(url('./files'), [
applyTemplates({
...strings,
...options,
}),
move(project.root),
]);

return chain([
addTailwindImport(stylesheetPath),
mergeWith(templateSource),
...TAILWIND_DEPENDENCIES.map((name) =>
addDependency(name, latestVersions[name], {
type: DependencyType.Dev,
existing: ExistingBehavior.Skip,
}),
),
]);
});
66 changes: 66 additions & 0 deletions packages/schematics/angular/tailwind/index_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Schema as ApplicationOptions, Style } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';

describe('Tailwind Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@schematics/angular',
require.resolve('../collection.json'),
);

const workspaceOptions: WorkspaceOptions = {
name: 'workspace',
newProjectRoot: 'projects',
version: '6.0.0',
};

const appOptions: ApplicationOptions = {
name: 'bar',
inlineStyle: false,
inlineTemplate: false,
routing: false,
style: Style.Css,
skipTests: false,
skipPackageJson: false,
};

let appTree: UnitTestTree;

beforeEach(async () => {
appTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
appTree = await schematicRunner.runSchematic('application', appOptions, appTree);
});

it('should add tailwind dependencies', async () => {
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
const packageJson = JSON.parse(tree.readContent('/package.json'));
expect(packageJson.devDependencies['tailwindcss']).toBeDefined();
expect(packageJson.devDependencies['postcss']).toBeDefined();
expect(packageJson.devDependencies['@tailwindcss/postcss']).toBeDefined();
});

it('should create a .postcssrc.json file in the project root', async () => {
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
expect(tree.exists('/projects/bar/.postcssrc.json')).toBe(true);
});

it('should configure tailwindcss plugin in .postcssrc.json', async () => {
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
const postCssConfig = JSON.parse(tree.readContent('/projects/bar/.postcssrc.json'));
expect(postCssConfig.plugins['@tailwindcss/postcss']).toBeDefined();
});

it('should add tailwind imports to styles.css', async () => {
const tree = await schematicRunner.runSchematic('tailwind', { project: 'bar' }, appTree);
const stylesContent = tree.readContent('/projects/bar/src/styles.css');
expect(stylesContent).toContain('@import "tailwindcss";');
});
});
15 changes: 15 additions & 0 deletions packages/schematics/angular/tailwind/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Tailwind CSS Schematic",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
}
},
"required": ["project"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"postcss": "^8.5.3",
"protractor": "~7.0.0",
"rxjs": "~7.8.0",
"tailwindcss": "^4.1.12",
"@tailwindcss/postcss": "^4.1.12",
"tslib": "^2.3.0",
"ts-node": "~10.9.0",
"typescript": "~5.9.2",
Expand Down