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 19858e9

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 19858e9

File tree

9 files changed

+202
-5
lines changed

9 files changed

+202
-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: 65 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,
@@ -24,12 +25,40 @@ import { getWorkspace as readWorkspace, updateWorkspace } from '../utility/works
2425
import { Builders as AngularBuilder } from '../utility/workspace-models';
2526
import { Schema as ConfigOptions, Type as ConfigType } from './schema';
2627

28+
const geminiFile: ContextFileInfo = { rulesName: 'GEMINI.md', directory: '.gemini' };
29+
const copilotFile: ContextFileInfo = {
30+
rulesName: 'copilot-instructions.md',
31+
directory: '.github',
32+
};
33+
const claudeFile: ContextFileInfo = { rulesName: 'CLAUDE.md', directory: '.claude' };
34+
const windsurfFile: ContextFileInfo = {
35+
rulesName: 'guidelines.md',
36+
directory: path.join('.windsurf', 'rules'),
37+
};
38+
39+
// Cursor file is a bit different, it has a front matter section.
40+
const cursorFile: ContextFileInfo = {
41+
rulesName: 'cursor.mdc',
42+
directory: path.join('.cursor', 'rules'),
43+
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
44+
};
45+
46+
const AI_TOOLS = {
47+
'gemini': geminiFile,
48+
'claude': claudeFile,
49+
'copilot': copilotFile,
50+
'cursor': cursorFile,
51+
'windsurf': windsurfFile,
52+
};
53+
2754
export default function (options: ConfigOptions): Rule {
2855
switch (options.type) {
2956
case ConfigType.Karma:
3057
return addKarmaConfig(options);
3158
case ConfigType.Browserslist:
3259
return addBrowserslistConfig(options);
60+
case ConfigType.Ai:
61+
return addAiContextFile(options);
3362
default:
3463
throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`);
3564
}
@@ -103,3 +132,39 @@ function addKarmaConfig(options: ConfigOptions): Rule {
103132
);
104133
});
105134
}
135+
136+
interface ContextFileInfo {
137+
rulesName: string;
138+
directory: string;
139+
frontmatter?: string;
140+
}
141+
142+
function addAiContextFile(options: ConfigOptions): Rule {
143+
const files: ContextFileInfo[] =
144+
options.tool === 'all' ? Object.values(AI_TOOLS) : [AI_TOOLS[options.tool!]];
145+
146+
return async (host) => {
147+
const workspace = await readWorkspace(host);
148+
const project = workspace.projects.get(options.project);
149+
if (!project) {
150+
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
151+
}
152+
153+
const rules = files.map(({ rulesName, directory, frontmatter }) =>
154+
mergeWith(
155+
apply(url('./files'), [
156+
// Keep only the single source template
157+
filter((p) => p.endsWith('__rulesName__.template')),
158+
applyTemplates({
159+
...strings,
160+
rulesName,
161+
frontmatter: frontmatter ?? '',
162+
}),
163+
move(directory),
164+
]),
165+
),
166+
);
167+
168+
return chain(rules);
169+
};
170+
}

‎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
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@angular-devkit/schematics/tasks';
2525
import { Schema as ApplicationOptions } from '../application/schema';
2626
import { Schema as WorkspaceOptions } from '../workspace/schema';
27+
import { Schema as ConfigOptions, Type as ConfigType, Tool as ConfigAiTool } from '../config/schema';
2728
import { Schema as NgNewOptions } from './schema';
2829

2930
export default function (options: NgNewOptions): Rule {
@@ -60,11 +61,20 @@ export default function (options: NgNewOptions): Rule {
6061
zoneless: options.zoneless,
6162
};
6263

64+
const configOptions: ConfigOptions = {
65+
project: options.name,
66+
type: ConfigType.Ai,
67+
tool: options.aiConfig as any,
68+
};
69+
70+
console.log("ai config", options.aiConfig, configOptions);
71+
6372
return chain([
6473
mergeWith(
6574
apply(empty(), [
6675
schematic('workspace', workspaceOptions),
6776
options.createApplication ? schematic('application', applicationOptions) : noop,
77+
options.aiConfig !== "none" ? schematic('config', configOptions) : noop,
6878
move(options.directory),
6979
]),
7080
),

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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' };
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(tree.readContent('/bar/.gemini/GEMINI.md')).toMatch(/^YouareanexpertinTypeScript/);
114+
});
106115
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@
144144
"x-prompt": "Do you want to create a 'zoneless' application without zone.js (Developer Preview)?",
145145
"type": "boolean",
146146
"default": false
147+
},
148+
"aiConfig": {
149+
"description": "Create an AI configuration file for the project. This file is used to improve the outputs of AI tools by following the best practices.",
150+
"default": "none",
151+
"type": "string",
152+
"enum": ["none", "gemini", "copilot", "claude", "cursor", "windsurf", "all"]
147153
}
148154
},
149155
"required": ["name", "version"]

0 commit comments

Comments
(0)

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