Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 53d6719

Browse files
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 53d6719

File tree

6 files changed

+153
-5
lines changed

6 files changed

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

‎packages/schematics/angular/config/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export default function (options: ConfigOptions): Rule {
3030
return addKarmaConfig(options);
3131
case ConfigType.Browserslist:
3232
return addBrowserslistConfig(options);
33+
case ConfigType.Ai:
34+
return addAiContextFile(options);
3335
default:
3436
throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`);
3537
}
@@ -103,3 +105,49 @@ function addKarmaConfig(options: ConfigOptions): Rule {
103105
);
104106
});
105107
}
108+
109+
function addAiContextFile(options: ConfigOptions): Rule {
110+
let fileName: string;
111+
let filePath: string;
112+
switch (options.tool) {
113+
case 'gemini':
114+
fileName = 'GEMINI.md';
115+
filePath = '.gemini';
116+
break;
117+
case 'copilot':
118+
fileName = 'copilot-instructions.md';
119+
filePath = '.github';
120+
break;
121+
case 'claude':
122+
fileName = 'CLAUDE.md';
123+
filePath = '.claude';
124+
break;
125+
case 'cursor':
126+
fileName = 'cursor.md';
127+
filePath = path.join('.cursor', 'rules');
128+
break;
129+
case 'windsurf':
130+
fileName = 'guidelines.md';
131+
filePath = path.join('.windsurf', 'rules');
132+
break;
133+
// TODO: should the default be 'all'?
134+
}
135+
136+
return async (host) => {
137+
const workspace = await readWorkspace(host);
138+
const project = workspace.projects.get(options.project);
139+
if (!project) {
140+
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
141+
}
142+
143+
return mergeWith(
144+
apply(url('./files'), [
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+
filter((p) => p.endsWith(`__rulesName__.template`)),
148+
applyTemplates({ rulesName: fileName, directoryName: filePath }),
149+
move(filePath),
150+
]),
151+
);
152+
};
153+
}

‎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, ToolasConfigTool,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: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,32 @@
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", "all"]
2530
}
2631
},
27-
"required": ["project", "type"]
32+
"required": ["project", "type"],
33+
"allOf": [
34+
{
35+
"if": {
36+
"properties": {
37+
"type": {
38+
"const": "ai"
39+
}
40+
}
41+
},
42+
"then": {
43+
"required": ["tool"]
44+
}
45+
}
46+
]
2847
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /