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 5ae2b77

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 37589b6 commit 5ae2b77

File tree

6 files changed

+189
-5
lines changed

6 files changed

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

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
SchematicsException,
1212
apply,
1313
applyTemplates,
14+
chain,
1415
filter,
1516
mergeWith,
1617
move,
@@ -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,77 @@ function addKarmaConfig(options: ConfigOptions): Rule {
103106
);
104107
});
105108
}
109+
110+
interface ContextFileInfo {
111+
rulesName: string;
112+
directory: string;
113+
frontmatter?: string;
114+
}
115+
116+
function addAiContextFile(options: ConfigOptions): Rule {
117+
const files: ContextFileInfo[] = [];
118+
119+
const geminiFile: ContextFileInfo = { rulesName: 'GEMINI.md', directory: '.gemini' };
120+
const copilotFile: ContextFileInfo = {
121+
rulesName: 'copilot-instructions.md',
122+
directory: '.github',
123+
};
124+
const claudeFile: ContextFileInfo = { rulesName: 'CLAUDE.md', directory: '.claude' };
125+
const windsurfFile: ContextFileInfo = {
126+
rulesName: 'guidelines.md',
127+
directory: path.join('.windsurf', 'rules'),
128+
};
129+
130+
// Cursor file is a bit different, it has a front matter section.
131+
const cursorFile: ContextFileInfo = {
132+
rulesName: 'cursor.mdc',
133+
directory: path.join('.cursor', 'rules'),
134+
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
135+
};
136+
137+
switch (options.tool) {
138+
case 'gemini':
139+
files.push(geminiFile);
140+
break;
141+
case 'claude':
142+
files.push(claudeFile);
143+
break;
144+
case 'copilot':
145+
files.push(copilotFile);
146+
break;
147+
case 'cursor':
148+
files.push(cursorFile);
149+
break;
150+
case 'windsurf':
151+
files.push(windsurfFile);
152+
break;
153+
case 'all':
154+
default:
155+
files.push(geminiFile, claudeFile, copilotFile, cursorFile, windsurfFile);
156+
}
157+
158+
return async (host) => {
159+
const workspace = await readWorkspace(host);
160+
const project = workspace.projects.get(options.project);
161+
if (!project) {
162+
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
163+
}
164+
165+
const rules = files.map(({ rulesName, directory, frontmatter }) =>
166+
mergeWith(
167+
apply(url('./files'), [
168+
// Keep only the single source template
169+
filter((p) => p.endsWith('__rulesName__.template')),
170+
applyTemplates({
171+
...strings,
172+
rulesName,
173+
frontmatter: frontmatter ?? '',
174+
}),
175+
move(directory),
176+
]),
177+
),
178+
);
179+
180+
return chain(rules);
181+
};
182+
}

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

Lines changed: 39 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,38 @@ 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')).toMatch(/^YouareanexpertinTypeScript/);
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+
const cursorFile = tree.readContent('.cursor/rules/cursor.mdc');
120+
expect(cursorFile).toContain('You are an expert in TypeScript');
121+
expect(cursorFile).toContain('context: true');
122+
expect(cursorFile).toContain('---\n\nYou are an expert in TypeScript');
123+
});
124+
125+
it('should create a windsurf file', async () => {
126+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Windsurf);
127+
expect(tree.readContent('.windsurf/rules/guidelines.md')).toContain(
128+
'You are an expert in TypeScript',
129+
);
130+
});
131+
132+
it('should create a claude file', async () => {
133+
const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Claude);
134+
expect(tree.readContent('.claude/CLAUDE.md')).toContain('You are an expert in TypeScript');
135+
});
136+
});
100137
});

‎packages/schematics/angular/config/schema.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,36 @@
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+
"not": {
39+
"const": "ai"
40+
}
41+
}
42+
}
43+
},
44+
"then": {
45+
"not": {
46+
"required": ["tool"]
47+
}
48+
}
49+
}
50+
]
2851
}

0 commit comments

Comments
(0)

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