@ng-annotate/angular 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-annotate/angular",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "schematics": "./schematics/collection.json",
5
5
  "description": "Angular library for ng-annotate-mcp — browser overlay for annotating components and routing instructions to an AI agent",
6
6
  "keywords": [
@@ -37,6 +37,8 @@
37
37
  "build": "ng-packagr -p ng-package.json && npm run build:schematics",
38
38
  "build:watch": "ng-packagr -p ng-package.json --watch",
39
39
  "build:schematics": "tsc -p schematics/tsconfig.json",
40
+ "test": "vitest run schematics",
41
+ "test:watch": "vitest schematics",
40
42
  "lint": "eslint src/",
41
43
  "lint:fix": "eslint src/ --fix"
42
44
  },
@@ -49,6 +51,7 @@
49
51
  "@angular/core": "^21.0.0",
50
52
  "ng-packagr": "^21.2.0",
51
53
  "rxjs": "^7.0.0",
52
- "typescript": "~5.9.0"
54
+ "typescript": "~5.9.0",
55
+ "vitest": "^4.0.18"
53
56
  }
54
57
  }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.insertAfterLastImport = insertAfterLastImport;
4
+ exports.insertIntoProviders = insertIntoProviders;
5
+ /** Insert `newImport` on the line after the last import statement (handles multi-line imports and blank lines between groups). */
6
+ function insertAfterLastImport(content, newImport) {
7
+ const lines = content.split('\n');
8
+ let lastImportLine = -1;
9
+ let inImport = false;
10
+ for (let i = 0; i < lines.length; i++) {
11
+ if (/^import\s/.test(lines[i]))
12
+ inImport = true;
13
+ if (inImport) {
14
+ lastImportLine = i;
15
+ if (lines[i].includes(';'))
16
+ inImport = false;
17
+ }
18
+ }
19
+ if (lastImportLine < 0)
20
+ return newImport + '\n' + content;
21
+ lines.splice(lastImportLine + 1, 0, newImport);
22
+ return lines.join('\n');
23
+ }
24
+ /** Insert `newProvider` as the first item in the `providers: [...]` array, preserving indentation style. */
25
+ function insertIntoProviders(content, newProvider) {
26
+ return content.replace(/^(\s*)providers\s*:\s*\[/m, (match, indent) => {
27
+ return `${indent}providers: [\n${indent} ${newProvider},`;
28
+ });
29
+ }
@@ -0,0 +1,25 @@
1
+ /** Insert `newImport` on the line after the last import statement (handles multi-line imports and blank lines between groups). */
2
+ export function insertAfterLastImport(content: string, newImport: string): string {
3
+ const lines = content.split('\n');
4
+ let lastImportLine = -1;
5
+ let inImport = false;
6
+
7
+ for (let i = 0; i < lines.length; i++) {
8
+ if (/^import\s/.test(lines[i])) inImport = true;
9
+ if (inImport) {
10
+ lastImportLine = i;
11
+ if (lines[i].includes(';')) inImport = false;
12
+ }
13
+ }
14
+
15
+ if (lastImportLine < 0) return newImport + '\n' + content;
16
+ lines.splice(lastImportLine + 1, 0, newImport);
17
+ return lines.join('\n');
18
+ }
19
+
20
+ /** Insert `newProvider` as the first item in the `providers: [...]` array, preserving indentation style. */
21
+ export function insertIntoProviders(content: string, newProvider: string): string {
22
+ return content.replace(/^(\s*)providers\s*:\s*\[/m, (match, indent) => {
23
+ return `${indent}providers: [\n${indent} ${newProvider},`;
24
+ });
25
+ }
@@ -38,31 +38,7 @@ const schematics_1 = require("@angular-devkit/schematics");
38
38
  const tasks_1 = require("@angular-devkit/schematics/tasks");
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
- /** Insert `newImport` on the line after the last import statement (handles multi-line imports and blank lines between groups). */
42
- function insertAfterLastImport(content, newImport) {
43
- const lines = content.split('\n');
44
- let lastImportLine = -1;
45
- let inImport = false;
46
- for (let i = 0; i < lines.length; i++) {
47
- if (/^import\s/.test(lines[i]))
48
- inImport = true;
49
- if (inImport) {
50
- lastImportLine = i;
51
- if (lines[i].includes(';'))
52
- inImport = false;
53
- }
54
- }
55
- if (lastImportLine < 0)
56
- return newImport + '\n' + content;
57
- lines.splice(lastImportLine + 1, 0, newImport);
58
- return lines.join('\n');
59
- }
60
- /** Insert `newProvider` as the first item in the `providers: [...]` array, preserving indentation style. */
61
- function insertIntoProviders(content, newProvider) {
62
- return content.replace(/^(\s*)providers\s*:\s*\[/m, (match, indent) => {
63
- return `${indent}providers: [\n${indent} ${newProvider},`;
64
- });
65
- }
41
+ const helpers_1 = require("./helpers");
66
42
  const MIN_ANGULAR_MAJOR = 21;
67
43
  function checkAngularVersion() {
68
44
  return (tree) => {
@@ -94,8 +70,15 @@ function addVitePlugin() {
94
70
  context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
95
71
  return;
96
72
  }
97
- content = insertAfterLastImport(content, "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';");
98
- content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
73
+ content = (0, helpers_1.insertAfterLastImport)(content, "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';");
74
+ if (/plugins\s*:\s*\[/.test(content)) {
75
+ // Existing plugins array — prepend into it
76
+ content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
77
+ }
78
+ else {
79
+ // No plugins array — inject one into defineConfig({...})
80
+ content = content.replace(/defineConfig\(\s*\{/, 'defineConfig({\n plugins: [...ngAnnotateMcp()],');
81
+ }
99
82
  tree.overwrite(viteConfigPath, content);
100
83
  context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
101
84
  };
@@ -119,8 +102,8 @@ function addProviders() {
119
102
  context.logger.info('provideNgAnnotate already present, skipping.');
120
103
  return;
121
104
  }
122
- content = insertAfterLastImport(content, "import { provideNgAnnotate } from '@ng-annotate/angular';");
123
- content = insertIntoProviders(content, 'provideNgAnnotate()');
105
+ content = (0, helpers_1.insertAfterLastImport)(content, "import { provideNgAnnotate } from '@ng-annotate/angular';");
106
+ content = (0, helpers_1.insertIntoProviders)(content, 'provideNgAnnotate()');
124
107
  tree.overwrite(appConfigPath, content);
125
108
  context.logger.info(`✅ Added provideNgAnnotate() to ${appConfigPath}`);
126
109
  };
@@ -0,0 +1,186 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
3
+ import { Tree } from '@angular-devkit/schematics';
4
+ import * as path from 'path';
5
+ import { insertAfterLastImport, insertIntoProviders } from './helpers';
6
+
7
+ // ─── Helper unit tests ────────────────────────────────────────────────────────
8
+
9
+ describe('insertAfterLastImport', () => {
10
+ it('inserts after a single import', () => {
11
+ const input = `import { Foo } from 'foo';\n\nexport const x = 1;\n`;
12
+ const result = insertAfterLastImport(input, "import { Bar } from 'bar';");
13
+ expect(result).toContain("import { Foo } from 'foo';\nimport { Bar } from 'bar';");
14
+ });
15
+
16
+ it('inserts after consecutive imports', () => {
17
+ const input = `import { A } from 'a';\nimport { B } from 'b';\n\nexport const x = 1;\n`;
18
+ const result = insertAfterLastImport(input, "import { C } from 'c';");
19
+ expect(result).toContain("import { B } from 'b';\nimport { C } from 'c';");
20
+ expect(result).not.toContain("import { A } from 'a';\nimport { C }");
21
+ });
22
+
23
+ it('inserts after last group when imports are separated by blank lines', () => {
24
+ const input = `import { A } from 'a';\nimport { B } from 'b';\n\nimport { C } from 'c';\n\nexport const x = 1;\n`;
25
+ const result = insertAfterLastImport(input, "import { D } from 'd';");
26
+ expect(result).toContain("import { C } from 'c';\nimport { D } from 'd';");
27
+ });
28
+
29
+ it('handles multi-line imports', () => {
30
+ const input = `import {\n ApplicationConfig,\n provideZoneChangeDetection,\n} from '@angular/core';\nimport { provideRouter } from '@angular/router';\n\nexport const x = 1;\n`;
31
+ const result = insertAfterLastImport(input, "import { provideNgAnnotate } from '@ng-annotate/angular';");
32
+ // Should insert after the last complete import, not inside the multi-line one
33
+ expect(result).toContain("import { provideRouter } from '@angular/router';\nimport { provideNgAnnotate }");
34
+ expect(result).not.toMatch(/import \{\nimport/);
35
+ });
36
+
37
+ it('prepends when no imports exist', () => {
38
+ const input = `export const x = 1;\n`;
39
+ const result = insertAfterLastImport(input, "import { Foo } from 'foo';");
40
+ expect(result).toMatch(/^import \{ Foo \}/);
41
+ });
42
+ });
43
+
44
+ describe('insertIntoProviders', () => {
45
+ it('inserts into inline providers array', () => {
46
+ const input = `export const appConfig = {\n providers: [provideRouter(routes)],\n};\n`;
47
+ const result = insertIntoProviders(input, 'provideNgAnnotate()');
48
+ expect(result).toContain('providers: [\n provideNgAnnotate(),');
49
+ expect(result).toContain('provideRouter(routes)');
50
+ });
51
+
52
+ it('inserts into multi-line providers array', () => {
53
+ const input = `export const appConfig = {\n providers: [\n provideRouter(routes),\n ],\n};\n`;
54
+ const result = insertIntoProviders(input, 'provideNgAnnotate()');
55
+ expect(result).toContain('providers: [\n provideNgAnnotate(),');
56
+ });
57
+
58
+ it('respects existing indentation', () => {
59
+ const input = `export const appConfig = {\n providers: [provideRouter(routes)],\n};\n`;
60
+ const result = insertIntoProviders(input, 'provideNgAnnotate()');
61
+ expect(result).toContain(' providers: [\n provideNgAnnotate(),');
62
+ });
63
+ });
64
+
65
+ // ─── Schematic integration tests ─────────────────────────────────────────────
66
+
67
+ const collectionPath = path.join(__dirname, '../collection.json');
68
+ const runner = new SchematicTestRunner('ng-annotate', collectionPath);
69
+
70
+ const BASE_PKG = JSON.stringify({
71
+ dependencies: { '@angular/core': '^21.0.0' },
72
+ devDependencies: {},
73
+ });
74
+
75
+ async function runSchematic(tree: UnitTestTree): Promise<UnitTestTree> {
76
+ return runner.runSchematic('ng-add', { aiTool: 'claude-code' }, tree);
77
+ }
78
+
79
+ function makeTree(files: Record<string, string>): Tree {
80
+ const tree = Tree.empty();
81
+ for (const [filePath, content] of Object.entries(files)) {
82
+ tree.create(filePath, content);
83
+ }
84
+ return tree;
85
+ }
86
+
87
+ describe('ng-add schematic — addVitePlugin', () => {
88
+ it('creates vite.config.ts when none exists', async () => {
89
+ const tree = makeTree({ 'package.json': BASE_PKG });
90
+ const result = await runSchematic(tree);
91
+ expect(result.exists('vite.config.ts')).toBe(true);
92
+ const content = result.readText('vite.config.ts');
93
+ expect(content).toContain("import { ngAnnotateMcp } from '@ng-annotate/vite-plugin'");
94
+ expect(content).toContain('plugins: [...ngAnnotateMcp()]');
95
+ });
96
+
97
+ it('adds plugin to existing vite.config.ts with a plugins array', async () => {
98
+ const tree = makeTree({
99
+ 'package.json': BASE_PKG,
100
+ 'vite.config.ts': `import { defineConfig } from 'vite';\nimport { foo } from 'foo';\n\nexport default defineConfig({\n plugins: [foo()],\n});\n`,
101
+ });
102
+ const result = await runSchematic(tree);
103
+ const content = result.readText('vite.config.ts');
104
+ expect(content).toContain("import { ngAnnotateMcp } from '@ng-annotate/vite-plugin'");
105
+ expect(content).toContain('plugins: [...ngAnnotateMcp(), ');
106
+ });
107
+
108
+ it('adds plugins array to vite.config.ts that has defineConfig({}) with no plugins', async () => {
109
+ const tree = makeTree({
110
+ 'package.json': BASE_PKG,
111
+ 'vite.config.ts': `import { defineConfig } from 'vite';\nimport { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n\nexport default defineConfig({});\n`,
112
+ });
113
+ // Already has the import but no plugins — should not be skipped
114
+ // Re-create without the import to test the injection path
115
+ const tree2 = makeTree({
116
+ 'package.json': BASE_PKG,
117
+ 'vite.config.ts': `import { defineConfig } from 'vite';\n\nexport default defineConfig({});\n`,
118
+ });
119
+ const result = await runSchematic(tree2);
120
+ const content = result.readText('vite.config.ts');
121
+ expect(content).toContain('plugins: [...ngAnnotateMcp()]');
122
+ });
123
+
124
+ it('skips when @ng-annotate/vite-plugin already present', async () => {
125
+ const original = `import { defineConfig } from 'vite';\nimport { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n\nexport default defineConfig({\n plugins: [...ngAnnotateMcp()],\n});\n`;
126
+ const tree = makeTree({ 'package.json': BASE_PKG, 'vite.config.ts': original });
127
+ const result = await runSchematic(tree);
128
+ expect(result.readText('vite.config.ts')).toBe(original);
129
+ });
130
+ });
131
+
132
+ describe('ng-add schematic — addProviders', () => {
133
+ it('adds import and provideNgAnnotate() to standard app.config.ts', async () => {
134
+ const tree = makeTree({
135
+ 'package.json': BASE_PKG,
136
+ 'src/app/app.config.ts': `import { ApplicationConfig } from '@angular/core';\nimport { provideRouter } from '@angular/router';\n\nimport { routes } from './app.routes';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideRouter(routes)],\n};\n`,
137
+ });
138
+ const result = await runSchematic(tree);
139
+ const content = result.readText('src/app/app.config.ts');
140
+ expect(content).toContain("import { provideNgAnnotate } from '@ng-annotate/angular'");
141
+ expect(content).toContain('provideNgAnnotate()');
142
+ });
143
+
144
+ it('handles multi-line imports without breaking them', async () => {
145
+ const tree = makeTree({
146
+ 'package.json': BASE_PKG,
147
+ 'src/app/app.config.ts': `import {\n ApplicationConfig,\n provideZoneChangeDetection,\n} from '@angular/core';\nimport { provideRouter } from '@angular/router';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)],\n};\n`,
148
+ });
149
+ const result = await runSchematic(tree);
150
+ const content = result.readText('src/app/app.config.ts');
151
+ // Import should not be inserted inside the multi-line import block
152
+ expect(content).not.toMatch(/import \{\nimport/);
153
+ expect(content).toContain("import { provideNgAnnotate } from '@ng-annotate/angular'");
154
+ expect(content).toContain('provideNgAnnotate()');
155
+ });
156
+
157
+ it('skips when provideNgAnnotate already present', async () => {
158
+ const original = `import { provideNgAnnotate } from '@ng-annotate/angular';\n\nexport const appConfig = {\n providers: [provideNgAnnotate()],\n};\n`;
159
+ const tree = makeTree({ 'package.json': BASE_PKG, 'src/app/app.config.ts': original });
160
+ const result = await runSchematic(tree);
161
+ expect(result.readText('src/app/app.config.ts')).toBe(original);
162
+ });
163
+ });
164
+
165
+ describe('ng-add schematic — addGitignore', () => {
166
+ it('creates .gitignore with .ng-annotate/ when none exists', async () => {
167
+ const tree = makeTree({ 'package.json': BASE_PKG });
168
+ const result = await runSchematic(tree);
169
+ expect(result.readText('.gitignore')).toContain('.ng-annotate/');
170
+ });
171
+
172
+ it('appends to existing .gitignore', async () => {
173
+ const tree = makeTree({ 'package.json': BASE_PKG, '.gitignore': 'node_modules/\ndist/\n' });
174
+ const result = await runSchematic(tree);
175
+ const content = result.readText('.gitignore');
176
+ expect(content).toContain('node_modules/');
177
+ expect(content).toContain('.ng-annotate/');
178
+ });
179
+
180
+ it('skips if .ng-annotate/ already in .gitignore', async () => {
181
+ const original = 'node_modules/\n.ng-annotate/\n';
182
+ const tree = makeTree({ 'package.json': BASE_PKG, '.gitignore': original });
183
+ const result = await runSchematic(tree);
184
+ expect(result.readText('.gitignore')).toBe(original);
185
+ });
186
+ });
@@ -2,32 +2,7 @@ import { Rule, SchematicContext, Tree, chain, SchematicsException } from '@angul
2
2
  import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
-
6
- /** Insert `newImport` on the line after the last import statement (handles multi-line imports and blank lines between groups). */
7
- function insertAfterLastImport(content: string, newImport: string): string {
8
- const lines = content.split('\n');
9
- let lastImportLine = -1;
10
- let inImport = false;
11
-
12
- for (let i = 0; i < lines.length; i++) {
13
- if (/^import\s/.test(lines[i])) inImport = true;
14
- if (inImport) {
15
- lastImportLine = i;
16
- if (lines[i].includes(';')) inImport = false;
17
- }
18
- }
19
-
20
- if (lastImportLine < 0) return newImport + '\n' + content;
21
- lines.splice(lastImportLine + 1, 0, newImport);
22
- return lines.join('\n');
23
- }
24
-
25
- /** Insert `newProvider` as the first item in the `providers: [...]` array, preserving indentation style. */
26
- function insertIntoProviders(content: string, newProvider: string): string {
27
- return content.replace(/^(\s*)providers\s*:\s*\[/m, (match, indent) => {
28
- return `${indent}providers: [\n${indent} ${newProvider},`;
29
- });
30
- }
5
+ import { insertAfterLastImport, insertIntoProviders } from './helpers';
31
6
 
32
7
  interface Options {
33
8
  aiTool: 'claude-code' | 'vscode' | 'both' | 'other';
@@ -78,7 +53,14 @@ function addVitePlugin(): Rule {
78
53
  }
79
54
 
80
55
  content = insertAfterLastImport(content, "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';");
81
- content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
56
+
57
+ if (/plugins\s*:\s*\[/.test(content)) {
58
+ // Existing plugins array — prepend into it
59
+ content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
60
+ } else {
61
+ // No plugins array — inject one into defineConfig({...})
62
+ content = content.replace(/defineConfig\(\s*\{/, 'defineConfig({\n plugins: [...ngAnnotateMcp()],');
63
+ }
82
64
 
83
65
  tree.overwrite(viteConfigPath, content);
84
66
  context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);