Skip to content

Commit 895a07f

Browse files
JeanMechealan-agius4
authored andcommitted
feat(@schematics/angular): add schematics to generate ai context files.
* `ng generate ai-config` to prompt support tools. * `ng generate ai-config --tool=gemini` to specify the tool. * `ng new` will prompt to config AI tools. * `ng new --ai-config=gemini` to create a new project with AI configuration. Supported AI tools: gemini, claude, copilot, windsurf, jetbrains, cursor.
1 parent 2e3cfd5 commit 895a07f

File tree

10 files changed

+303
-2
lines changed

10 files changed

+303
-2
lines changed

goldens/public-api/angular_devkit/schematics/index.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,10 @@ export enum MergeStrategy {
637637
export function mergeWith(source: Source, strategy?: MergeStrategy): Rule;
638638

639639
// @public (undocumented)
640-
export function move(from: string, to?: string): Rule;
640+
export function move(from: string, to: string): Rule;
641+
642+
// @public (undocumented)
643+
export function move(to: string): Rule;
641644

642645
// @public (undocumented)
643646
export function noop(): Rule;

packages/angular_devkit/schematics/src/rules/move.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { join, normalize } from '@angular-devkit/core';
1010
import { Rule } from '../engine/interface';
1111
import { noop } from './base';
1212

13+
export function move(from: string, to: string): Rule;
14+
export function move(to: string): Rule;
1315
export function move(from: string, to?: string): Rule {
1416
if (to === undefined) {
1517
to = from;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<% if (frontmatter) { %><%= frontmatter %>
2+
3+
<% } %>You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.
4+
5+
## TypeScript Best Practices
6+
7+
- Use strict type checking
8+
- Prefer type inference when the type is obvious
9+
- Avoid the `any` type; use `unknown` when type is uncertain
10+
11+
## Angular Best Practices
12+
13+
- Always use standalone components over NgModules
14+
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
15+
- Use signals for state management
16+
- Implement lazy loading for feature routes
17+
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
18+
- Use `NgOptimizedImage` for all static images.
19+
- `NgOptimizedImage` does not work for inline base64 images.
20+
21+
## Components
22+
23+
- Keep components small and focused on a single responsibility
24+
- Use `input()` and `output()` functions instead of decorators
25+
- Use `computed()` for derived state
26+
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
27+
- Prefer inline templates for small components
28+
- Prefer Reactive forms instead of Template-driven ones
29+
- Do NOT use `ngClass`, use `class` bindings instead
30+
- Do NOT use `ngStyle`, use `style` bindings instead
31+
32+
## State Management
33+
34+
- Use signals for local component state
35+
- Use `computed()` for derived state
36+
- Keep state transformations pure and predictable
37+
- Do NOT use `mutate` on signals, use `update` or `set` instead
38+
39+
## Templates
40+
41+
- Keep templates simple and avoid complex logic
42+
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
43+
- Use the async pipe to handle observables
44+
45+
## Services
46+
47+
- Design services around a single responsibility
48+
- Use the `providedIn: 'root'` option for singleton services
49+
- Use the `inject()` function instead of constructor injection
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
Rule,
11+
apply,
12+
applyTemplates,
13+
chain,
14+
filter,
15+
mergeWith,
16+
move,
17+
noop,
18+
strings,
19+
url,
20+
} from '@angular-devkit/schematics';
21+
import { posix as path } from 'node:path';
22+
import { Schema as ConfigOptions, Tool } from './schema';
23+
24+
type ToolWithoutNone = Exclude<Tool, Tool.None>;
25+
26+
const AI_TOOLS: { [key in ToolWithoutNone]: ContextFileInfo } = {
27+
gemini: {
28+
rulesName: 'GEMINI.md',
29+
directory: '.gemini',
30+
},
31+
claude: {
32+
rulesName: 'CLAUDE.md',
33+
directory: '.claude',
34+
},
35+
copilot: {
36+
rulesName: 'copilot-instructions.md',
37+
directory: '.github',
38+
},
39+
windsurf: {
40+
rulesName: 'guidelines.md',
41+
directory: path.join('.windsurf', 'rules'),
42+
},
43+
jetbrains: {
44+
rulesName: 'guidelines.md',
45+
directory: '.junie',
46+
},
47+
// Cursor file has a front matter section.
48+
cursor: {
49+
rulesName: 'cursor.mdc',
50+
directory: path.join('.cursor', 'rules'),
51+
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
52+
},
53+
};
54+
55+
interface ContextFileInfo {
56+
rulesName: string;
57+
directory: string;
58+
frontmatter?: string;
59+
}
60+
61+
export default function ({ tool }: ConfigOptions): Rule {
62+
if (!tool || tool.includes(Tool.None)) {
63+
return noop();
64+
}
65+
66+
const files: ContextFileInfo[] = (tool as ToolWithoutNone[]).map(
67+
(selectedTool) => AI_TOOLS[selectedTool],
68+
);
69+
70+
const rules = files.map(({ rulesName, directory, frontmatter }) =>
71+
mergeWith(
72+
apply(url('./files'), [
73+
applyTemplates({
74+
...strings,
75+
rulesName,
76+
frontmatter,
77+
}),
78+
move(directory),
79+
]),
80+
),
81+
);
82+
83+
return chain(rules);
84+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 } from '../application/schema';
11+
import { Schema as WorkspaceOptions } from '../workspace/schema';
12+
import { Schema as ConfigOptions, Tool as ConfigTool } from './schema';
13+
14+
describe('Ai Config Schematic', () => {
15+
const schematicRunner = new SchematicTestRunner(
16+
'@schematics/angular',
17+
require.resolve('../collection.json'),
18+
);
19+
20+
const workspaceOptions: WorkspaceOptions = {
21+
name: 'workspace',
22+
newProjectRoot: 'projects',
23+
version: '15.0.0',
24+
};
25+
26+
const defaultAppOptions: ApplicationOptions = {
27+
name: 'foo',
28+
inlineStyle: true,
29+
inlineTemplate: true,
30+
routing: false,
31+
skipPackageJson: false,
32+
};
33+
34+
let applicationTree: UnitTestTree;
35+
function runConfigSchematic(tool: ConfigTool[]): Promise<UnitTestTree> {
36+
return schematicRunner.runSchematic<ConfigOptions>('ai-config', { tool }, applicationTree);
37+
}
38+
39+
beforeEach(async () => {
40+
const workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
41+
applicationTree = await schematicRunner.runSchematic(
42+
'application',
43+
defaultAppOptions,
44+
workspaceTree,
45+
);
46+
});
47+
48+
it('should create a GEMINI.MD file', async () => {
49+
const tree = await runConfigSchematic([ConfigTool.Gemini]);
50+
expect(tree.exists('.gemini/GEMINI.md')).toBeTruthy();
51+
});
52+
53+
it('should create a copilot-instructions.md file', async () => {
54+
const tree = await runConfigSchematic([ConfigTool.Copilot]);
55+
expect(tree.exists('.github/copilot-instructions.md')).toBeTruthy();
56+
});
57+
58+
it('should create a cursor file', async () => {
59+
const tree = await runConfigSchematic([ConfigTool.Cursor]);
60+
expect(tree.exists('.cursor/rules/cursor.mdc')).toBeTruthy();
61+
});
62+
63+
it('should create a windsurf file', async () => {
64+
const tree = await runConfigSchematic([ConfigTool.Windsurf]);
65+
expect(tree.exists('.windsurf/rules/guidelines.md')).toBeTruthy();
66+
});
67+
68+
it('should create a claude file', async () => {
69+
const tree = await runConfigSchematic([ConfigTool.Claude]);
70+
expect(tree.exists('.claude/CLAUDE.md')).toBeTruthy();
71+
});
72+
73+
it('should create a jetbrains file', async () => {
74+
const tree = await runConfigSchematic([ConfigTool.Jetbrains]);
75+
expect(tree.exists('.junie/guidelines.md')).toBeTruthy();
76+
});
77+
78+
it('should create multiple files when multiple tools are selected', async () => {
79+
const tree = await runConfigSchematic([
80+
ConfigTool.Gemini,
81+
ConfigTool.Copilot,
82+
ConfigTool.Cursor,
83+
]);
84+
expect(tree.exists('.gemini/GEMINI.md')).toBeTruthy();
85+
expect(tree.exists('.github/copilot-instructions.md')).toBeTruthy();
86+
expect(tree.exists('.cursor/rules/cursor.mdc')).toBeTruthy();
87+
});
88+
89+
it('should error is None is associated with other values', () => {
90+
return expectAsync(runConfigSchematic([ConfigTool.None, ConfigTool.Cursor])).toBeRejected();
91+
});
92+
93+
it('should not create any files if None is selected', async () => {
94+
const filesCount = applicationTree.files.length;
95+
const tree = await runConfigSchematic([ConfigTool.None]);
96+
expect(tree.files.length).toBe(filesCount);
97+
});
98+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "SchematicsAngularConfig",
4+
"title": "Angular Config File Options Schema",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"description": "Generates AI configuration files for Angular projects. This schematic creates configuration files that help AI tools follow Angular best practices, improving the quality of AI-generated code and suggestions.",
8+
"properties": {
9+
"tool": {
10+
"type": "array",
11+
"uniqueItems": true,
12+
"default": "none",
13+
"x-prompt": "Which AI tools do you want to configure with Angular best practices? https://angular.dev/ai/develop-with-ai",
14+
"description": "Specifies which AI tools to generate configuration files for. These file are used to improve the outputs of AI tools by following the best practices.",
15+
"minItems": 1,
16+
"items": {
17+
"type": "string",
18+
"enum": ["none", "gemini", "copilot", "claude", "cursor", "jetbrains", "windsurf"]
19+
}
20+
}
21+
},
22+
"if": {
23+
"properties": {
24+
"tool": {
25+
"contains": {
26+
"const": "none"
27+
}
28+
}
29+
},
30+
"required": ["tool"]
31+
},
32+
"then": {
33+
"properties": {
34+
"tool": {
35+
"maxItems": 1
36+
}
37+
}
38+
}
39+
}

packages/schematics/angular/collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@
131131
"factory": "./config",
132132
"schema": "./config/schema.json",
133133
"description": "Generates a configuration file."
134+
},
135+
"ai-config": {
136+
"factory": "./ai-config",
137+
"schema": "./ai-config/schema.json",
138+
"description": "Generates an AI tool configuration file."
134139
}
135140
}
136141
}

packages/schematics/angular/ng-new/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export default function (options: NgNewOptions): Rule {
6565
apply(empty(), [
6666
schematic('workspace', workspaceOptions),
6767
options.createApplication ? schematic('application', applicationOptions) : noop,
68+
schematic('ai-config', {
69+
tool: options.aiConfig?.length ? options.aiConfig : undefined,
70+
}),
6871
move(options.directory),
6972
]),
7073
),

packages/schematics/angular/ng-new/index_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
10-
import { Schema as NgNewOptions } from './schema';
10+
import { AiConfig, Schema as NgNewOptions } from './schema';
1111

1212
describe('Ng New Schematic', () => {
1313
const schematicRunner = new SchematicTestRunner(
@@ -103,4 +103,13 @@ describe('Ng New Schematic', () => {
103103
const { cli } = JSON.parse(tree.readContent('/bar/angular.json'));
104104
expect(cli.packageManager).toBe('npm');
105105
});
106+
107+
it('should add ai config file when aiConfig is set', async () => {
108+
const options = { ...defaultOptions, aiConfig: ['gemini', 'claude'] };
109+
110+
const tree = await schematicRunner.runSchematic('ng-new', options);
111+
const files = tree.files;
112+
expect(files).toContain('/bar/.gemini/GEMINI.md');
113+
expect(files).toContain('/bar/.claude/CLAUDE.md');
114+
});
106115
});

packages/schematics/angular/ng-new/schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@
142142
"zoneless": {
143143
"description": "Create an initial application that does not utilize `zone.js`.",
144144
"type": "boolean"
145+
},
146+
"aiConfig": {
147+
"type": "array",
148+
"uniqueItems": true,
149+
"description": "Specifies which AI tools to generate configuration files for. These file are used to improve the outputs of AI tools by following the best practices.",
150+
"items": {
151+
"type": "string",
152+
"enum": ["none", "gemini", "copilot", "claude", "cursor", "jetbrains", "windsurf"]
153+
}
145154
}
146155
},
147156
"required": ["name", "version"]

0 commit comments

Comments
 (0)