Skip to content

Commit 736c88d

Browse files
committed
feat(@angular-devkit/schematics): add schematics to generate ai context files.
* `ng generate config ai` to prompt support tools. * `ng generate config ai --tool=gemini` to specify the tool. Supported ai tools: gemini, claude, copilot, windsurf, cursor.
1 parent 5d085ee commit 736c88d

File tree

7 files changed

+156
-3
lines changed

7 files changed

+156
-3
lines changed

packages/angular_devkit/schematics/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from './rules/base';
2323
export * from './rules/call';
2424
export * from './rules/move';
2525
export * from './rules/random';
26+
export * from './rules/rename';
2627
export * from './rules/schematic';
2728
export * from './rules/template';
2829
export * from './rules/url';

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 { posix as path } from 'node:path';
10+
import { Rule } from '../engine';
11+
12+
export function rename(filePath: string, fromFileName: string, fileName: string): Rule {
13+
return (tree) => {
14+
tree.rename(path.join(filePath, fromFileName), path.join(filePath, fileName));
15+
16+
return tree;
17+
};
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
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.
2+
## TypeScript Best Practices
3+
- Use strict type checking
4+
- Prefer type inference when the type is obvious
5+
- Avoid the `any` type; use `unknown` when type is uncertain
6+
## Angular Best Practices
7+
- Always use standalone components over NgModules
8+
- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators
9+
- Use signals for state management
10+
- Implement lazy loading for feature routes
11+
- Use `NgOptimizedImage` for all static images.
12+
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
13+
## Components
14+
- Keep components small and focused on a single responsibility
15+
- Use `input()` and `output()` functions instead of decorators
16+
- Use `computed()` for derived state
17+
- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
18+
- Prefer inline templates for small components
19+
- Prefer Reactive forms instead of Template-driven ones
20+
- Do NOT use `ngClass`, use `class` bindings instead
21+
- DO NOT use `ngStyle`, use `style` bindings instead
22+
## State Management
23+
- Use signals for local component state
24+
- Use `computed()` for derived state
25+
- Keep state transformations pure and predictable
26+
- Do NOT use `mutate` on signals, use `update` or `set` instead
27+
## Templates
28+
- Keep templates simple and avoid complex logic
29+
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
30+
- Use the async pipe to handle observables
31+
## Services
32+
- Design services around a single responsibility
33+
- Use the `providedIn: 'root'` option for singleton services
34+
- Use the `inject()` function instead of constructor injection

packages/schematics/angular/config/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
filter,
1515
mergeWith,
1616
move,
17+
rename,
1718
strings,
1819
url,
1920
} from '@angular-devkit/schematics';
@@ -30,6 +31,8 @@ export default function (options: ConfigOptions): Rule {
3031
return addKarmaConfig(options);
3132
case ConfigType.Browserslist:
3233
return addBrowserslistConfig(options);
34+
case ConfigType.Ai:
35+
return addAiContextFile(options);
3336
default:
3437
throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`);
3538
}
@@ -103,3 +106,53 @@ function addKarmaConfig(options: ConfigOptions): Rule {
103106
);
104107
});
105108
}
109+
110+
function addAiContextFile(options: ConfigOptions): Rule {
111+
let fileName: string;
112+
let filePath: string;
113+
switch (options.tool) {
114+
case 'gemini':
115+
fileName = 'GEMINI.md';
116+
filePath = '.gemini';
117+
break;
118+
case 'copilot':
119+
fileName = 'copilot-instructions.md';
120+
filePath = '.github';
121+
break;
122+
case 'claude':
123+
fileName = 'CLAUDE.md';
124+
filePath = '.claude';
125+
break;
126+
case 'cursor':
127+
fileName = 'cursor.md';
128+
filePath = path.join('.cursor', 'rules');
129+
break;
130+
case 'windsurf':
131+
fileName = 'guidelines.md';
132+
filePath = path.join('.windsurf', 'rules');
133+
break;
134+
default:
135+
throw new SchematicsException(`Unsupported AI tool: ${options.tool}`);
136+
}
137+
138+
return async (host) => {
139+
const workspace = await readWorkspace(host);
140+
const project = workspace.projects.get(options.project);
141+
if (!project) {
142+
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
143+
}
144+
145+
// Keep this file in sync with the one presented here https://angular.dev/ai/develop-with-ai
146+
// The file is located in the framework repo at adev/src/context/best-practices.md
147+
const templateName = 'best-practices.md';
148+
149+
return mergeWith(
150+
apply(url('./files'), [
151+
filter((p) => p.endsWith(`${templateName}.template`)),
152+
applyTemplates({}),
153+
move(filePath),
154+
rename(filePath, templateName, fileName),
155+
]),
156+
);
157+
};
158+
}

packages/schematics/angular/config/index_spec.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
1010
import { Schema as ApplicationOptions } from '../application/schema';
1111
import { Schema as WorkspaceOptions } from '../workspace/schema';
12-
import { Schema as ConfigOptions, Type as ConfigType } from './schema';
12+
import { Schema as ConfigOptions, Tool as ConfigTool, Type as ConfigType } from './schema';
1313

1414
describe('Config Schematic', () => {
1515
const schematicRunner = new SchematicTestRunner(
@@ -32,12 +32,15 @@ describe('Config Schematic', () => {
3232
};
3333

3434
let applicationTree: UnitTestTree;
35-
function runConfigSchematic(type: ConfigType): Promise<UnitTestTree> {
35+
function runConfigSchematic(type: ConfigType, tool?: ConfigTool): Promise<UnitTestTree>;
36+
function runConfigSchematic(type: ConfigType.Ai, tool: ConfigTool): Promise<UnitTestTree>;
37+
function runConfigSchematic(type: ConfigType, tool?: ConfigTool): Promise<UnitTestTree> {
3638
return schematicRunner.runSchematic<ConfigOptions>(
3739
'config',
3840
{
3941
project: 'foo',
4042
type,
43+
tool,
4144
},
4245
applicationTree,
4346
);
@@ -97,4 +100,37 @@ describe('Config Schematic', () => {
97100
expect(tree.readContent('projects/foo/.browserslistrc')).toContain('Chrome >=');
98101
});
99102
});
103+
104+
describe(`when 'type' is 'ai'`, () => {
105+
it('should create a GEMINI.MD file', async () => {
106+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Gemini);
107+
expect(tree.readContent('.gemini/GEMINI.md')).toContain('You are an expert in TypeScript');
108+
});
109+
110+
it('should create a copilot-instructions.md file', async () => {
111+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Copilot);
112+
expect(tree.readContent('.github/copilot-instructions.md')).toContain(
113+
'You are an expert in TypeScript',
114+
);
115+
});
116+
117+
it('should create a cursor file', async () => {
118+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Cursor);
119+
expect(tree.readContent('.cursor/rules/cursor.md')).toContain(
120+
'You are an expert in TypeScript',
121+
);
122+
});
123+
124+
it('should create a windsurf file', async () => {
125+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Windsurf);
126+
expect(tree.readContent('.windsurf/rules/guidelines.md')).toContain(
127+
'You are an expert in TypeScript',
128+
);
129+
});
130+
131+
it('should create a claude file', async () => {
132+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Claude);
133+
expect(tree.readContent('.claude/CLAUDE.md')).toContain('You are an expert in TypeScript');
134+
});
135+
});
100136
});

packages/schematics/angular/config/schema.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,21 @@
1616
"type": {
1717
"type": "string",
1818
"description": "Specifies the type of configuration file to generate.",
19-
"enum": ["karma", "browserslist"],
19+
"enum": ["karma", "browserslist", "ai"],
2020
"x-prompt": "Which type of configuration file would you like to create?",
2121
"$default": {
2222
"$source": "argv",
2323
"index": 0
2424
}
25+
},
26+
"tool": {
27+
"type": "string",
28+
"description": "Specifies the AI tool to configure when type is 'ai'.",
29+
"enum": ["gemini", "copilot", "claude", "cursor", "windsurf"],
30+
"x-prompt": {
31+
"message": "Which AI tool would you like to configure?",
32+
"when": "type === 'ai'"
33+
}
2534
}
2635
},
2736
"required": ["project", "type"]

0 commit comments

Comments
 (0)