diff --git a/goldens/public-api/angular_devkit/schematics/index.api.md b/goldens/public-api/angular_devkit/schematics/index.api.md index 3e3a0bc97ead..505bd2c39920 100644 --- a/goldens/public-api/angular_devkit/schematics/index.api.md +++ b/goldens/public-api/angular_devkit/schematics/index.api.md @@ -637,7 +637,10 @@ export enum MergeStrategy { export function mergeWith(source: Source, strategy?: MergeStrategy): Rule; // @public (undocumented) -export function move(from: string, to?: string): Rule; +export function move(from: string, to: string): Rule; + +// @public (undocumented) +export function move(to: string): Rule; // @public (undocumented) export function noop(): Rule; diff --git a/packages/angular_devkit/schematics/src/rules/move.ts b/packages/angular_devkit/schematics/src/rules/move.ts index 05cd2b36634e..4c6c1e8d2f39 100644 --- a/packages/angular_devkit/schematics/src/rules/move.ts +++ b/packages/angular_devkit/schematics/src/rules/move.ts @@ -10,6 +10,8 @@ import { join, normalize } from '@angular-devkit/core'; import { Rule } from '../engine/interface'; import { noop } from './base'; +export function move(from: string, to: string): Rule; +export function move(to: string): Rule; export function move(from: string, to?: string): Rule { if (to === undefined) { to = from; diff --git a/packages/schematics/angular/config/files/__rulesName__.template b/packages/schematics/angular/config/files/__rulesName__.template new file mode 100644 index 000000000000..7f426527f5cb --- /dev/null +++ b/packages/schematics/angular/config/files/__rulesName__.template @@ -0,0 +1,42 @@ +<% if (frontmatter) { %><%= frontmatter %> + +<% } %>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. + +## TypeScript Best Practices +- Use strict type checking +- Prefer type inference when the type is obvious +- Avoid the `any` type; use `unknown` when type is uncertain + +## Angular Best Practices +- Always use standalone components over NgModules +- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators +- Use signals for state management +- Implement lazy loading for feature routes +- Use `NgOptimizedImage` for all static images. +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead + +## Components +- Keep components small and focused on a single responsibility +- Use `input()` and `output()` functions instead of decorators +- Use `computed()` for derived state +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator +- Prefer inline templates for small components +- Prefer Reactive forms instead of Template-driven ones +- Do NOT use `ngClass`, use `class` bindings instead +- DO NOT use `ngStyle`, use `style` bindings instead + +## State Management +- Use signals for local component state +- Use `computed()` for derived state +- Keep state transformations pure and predictable +- Do NOT use `mutate` on signals, use `update` or `set` instead + +## Templates +- Keep templates simple and avoid complex logic +- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Use the async pipe to handle observables + +## Services +- Design services around a single responsibility +- Use the `providedIn: 'root'` option for singleton services +- Use the `inject()` function instead of constructor injection diff --git a/packages/schematics/angular/config/index.ts b/packages/schematics/angular/config/index.ts index 5878bd8c498a..f8894cfc32f0 100644 --- a/packages/schematics/angular/config/index.ts +++ b/packages/schematics/angular/config/index.ts @@ -11,6 +11,7 @@ import { SchematicsException, apply, applyTemplates, + chain, filter, mergeWith, move, @@ -30,6 +31,8 @@ export default function (options: ConfigOptions): Rule { return addKarmaConfig(options); case ConfigType.Browserslist: return addBrowserslistConfig(options); + case ConfigType.Ai: + return addAiContextFile(options); default: throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`); } @@ -103,3 +106,77 @@ function addKarmaConfig(options: ConfigOptions): Rule { ); }); } + +interface ContextFileInfo { + rulesName: string; + directory: string; + frontmatter?: string; +} + +function addAiContextFile(options: ConfigOptions): Rule { + const files: ContextFileInfo[] = []; + + const geminiFile: ContextFileInfo = { rulesName: 'GEMINI.md', directory: '.gemini' }; + const copilotFile: ContextFileInfo = { + rulesName: 'copilot-instructions.md', + directory: '.github', + }; + const claudeFile: ContextFileInfo = { rulesName: 'CLAUDE.md', directory: '.claude' }; + const windsurfFile: ContextFileInfo = { + rulesName: 'guidelines.md', + directory: path.join('.windsurf', 'rules'), + }; + + // Cursor file is a bit different, it has a front matter section. + const cursorFile: ContextFileInfo = { + rulesName: 'cursor.mdc', + directory: path.join('.cursor', 'rules'), + frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`, + }; + + switch (options.tool) { + case 'gemini': + files.push(geminiFile); + break; + case 'claude': + files.push(claudeFile); + break; + case 'copilot': + files.push(copilotFile); + break; + case 'cursor': + files.push(cursorFile); + break; + case 'windsurf': + files.push(windsurfFile); + break; + case 'all': + default: + files.push(geminiFile, claudeFile, copilotFile, cursorFile, windsurfFile); + } + + return async (host) => { + const workspace = await readWorkspace(host); + const project = workspace.projects.get(options.project); + if (!project) { + throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`); + } + + const rules = files.map(({ rulesName, directory, frontmatter }) => + mergeWith( + apply(url('./files'), [ + // Keep only the single source template + filter((p) => p.endsWith('__rulesName__.template')), + applyTemplates({ + ...strings, + rulesName, + frontmatter: frontmatter ?? '', + }), + move(directory), + ]), + ), + ); + + return chain(rules); + }; +} diff --git a/packages/schematics/angular/config/index_spec.ts b/packages/schematics/angular/config/index_spec.ts index c9349bcb609d..68451cb3b762 100644 --- a/packages/schematics/angular/config/index_spec.ts +++ b/packages/schematics/angular/config/index_spec.ts @@ -9,7 +9,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { Schema as ApplicationOptions } from '../application/schema'; import { Schema as WorkspaceOptions } from '../workspace/schema'; -import { Schema as ConfigOptions, Type as ConfigType } from './schema'; +import { Schema as ConfigOptions, Tool as ConfigTool, Type as ConfigType } from './schema'; describe('Config Schematic', () => { const schematicRunner = new SchematicTestRunner( @@ -32,12 +32,15 @@ describe('Config Schematic', () => { }; let applicationTree: UnitTestTree; - function runConfigSchematic(type: ConfigType): Promise { + function runConfigSchematic(type: ConfigType, tool?: ConfigTool): Promise; + function runConfigSchematic(type: ConfigType.Ai, tool: ConfigTool): Promise; + function runConfigSchematic(type: ConfigType, tool?: ConfigTool): Promise { return schematicRunner.runSchematic( 'config', { project: 'foo', type, + tool, }, applicationTree, ); @@ -97,4 +100,38 @@ describe('Config Schematic', () => { expect(tree.readContent('projects/foo/.browserslistrc')).toContain('Chrome >='); }); }); + + describe(`when 'type' is 'ai'`, () => { + it('should create a GEMINI.MD file', async () => { + const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Gemini); + expect(tree.readContent('.gemini/GEMINI.md')).toMatch(/^You are an expert in TypeScript/); + }); + + it('should create a copilot-instructions.md file', async () => { + const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Copilot); + expect(tree.readContent('.github/copilot-instructions.md')).toContain( + 'You are an expert in TypeScript', + ); + }); + + it('should create a cursor file', async () => { + const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Cursor); + const cursorFile = tree.readContent('.cursor/rules/cursor.mdc'); + expect(cursorFile).toContain('You are an expert in TypeScript'); + expect(cursorFile).toContain('context: true'); + expect(cursorFile).toContain('---\n\nYou are an expert in TypeScript'); + }); + + it('should create a windsurf file', async () => { + const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Windsurf); + expect(tree.readContent('.windsurf/rules/guidelines.md')).toContain( + 'You are an expert in TypeScript', + ); + }); + + it('should create a claude file', async () => { + const tree = await runConfigSchematic(ConfigType.Ai, ConfigTool.Claude); + expect(tree.readContent('.claude/CLAUDE.md')).toContain('You are an expert in TypeScript'); + }); + }); }); diff --git a/packages/schematics/angular/config/schema.json b/packages/schematics/angular/config/schema.json index 14bb34f07260..ddd84d072dc8 100644 --- a/packages/schematics/angular/config/schema.json +++ b/packages/schematics/angular/config/schema.json @@ -16,12 +16,17 @@ "type": { "type": "string", "description": "Specifies the type of configuration file to generate.", - "enum": ["karma", "browserslist"], + "enum": ["karma", "browserslist", "ai"], "x-prompt": "Which type of configuration file would you like to create?", "$default": { "$source": "argv", "index": 0 } + }, + "tool": { + "type": "string", + "description": "Specifies the AI tool to configure when type is 'ai'.", + "enum": ["gemini", "copilot", "claude", "cursor", "windsurf", "all"] } }, "required": ["project", "type"]