@ng-annotate/angular 0.4.1 → 0.5.1

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/builders.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "@angular-devkit/architect/builders-schema.json",
3
+ "builders": {
4
+ "dev-server": {
5
+ "implementation": "./dist/builders/dev-server/index",
6
+ "schema": "./dist/builders/dev-server/schema.json",
7
+ "description": "ng-annotate dev server — wraps @angular/build:dev-server with integrated WebSocket support and component manifest injection. No proxy configuration required."
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { DevServerBuilderOptions } from '@angular/build';
2
+ declare const _default: import("@angular-devkit/architect").Builder<DevServerBuilderOptions & import("@angular-devkit/core").JsonObject>;
3
+ export default _default;
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const architect_1 = require("@angular-devkit/architect");
7
+ const build_1 = require("@angular/build");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_crypto_1 = require("node:crypto");
11
+ const ws_1 = require("ws");
12
+ // ─── Store ────────────────────────────────────────────────────────────────────
13
+ const STORE_DIR = '.ng-annotate';
14
+ function makeStore(projectRoot) {
15
+ const storePath = node_path_1.default.join(projectRoot, STORE_DIR, 'store.json');
16
+ function ensureStore() {
17
+ const dir = node_path_1.default.dirname(storePath);
18
+ if (!node_fs_1.default.existsSync(dir))
19
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
20
+ if (!node_fs_1.default.existsSync(storePath)) {
21
+ node_fs_1.default.writeFileSync(storePath, JSON.stringify({ sessions: {}, annotations: {} }, null, 2), 'utf8');
22
+ }
23
+ }
24
+ let writeQueue = Promise.resolve();
25
+ async function withLock(fn) {
26
+ ensureStore();
27
+ const result = writeQueue.then(() => {
28
+ const raw = node_fs_1.default.readFileSync(storePath, 'utf8');
29
+ const data = JSON.parse(raw);
30
+ const updated = fn(data);
31
+ node_fs_1.default.writeFileSync(storePath, JSON.stringify(updated, null, 2), 'utf8');
32
+ return updated;
33
+ });
34
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
35
+ writeQueue = result.catch(() => { });
36
+ return result;
37
+ }
38
+ function readStore() {
39
+ ensureStore();
40
+ return JSON.parse(node_fs_1.default.readFileSync(storePath, 'utf8'));
41
+ }
42
+ return {
43
+ async createSession(url) {
44
+ const now = new Date().toISOString();
45
+ const session = { id: (0, node_crypto_1.randomUUID)(), createdAt: now, lastSeenAt: now, active: true, url };
46
+ await withLock((data) => { data.sessions[session.id] = session; return data; });
47
+ return session;
48
+ },
49
+ async updateSession(id, patch) {
50
+ await withLock((data) => {
51
+ const s = data.sessions[id];
52
+ if (s)
53
+ data.sessions[id] = { ...s, ...patch, id };
54
+ return data;
55
+ });
56
+ },
57
+ async createAnnotation(payload) {
58
+ const annotation = {
59
+ id: (0, node_crypto_1.randomUUID)(),
60
+ createdAt: new Date().toISOString(),
61
+ status: 'pending',
62
+ replies: [],
63
+ ...payload,
64
+ };
65
+ await withLock((data) => { data.annotations[annotation.id] = annotation; return data; });
66
+ return annotation;
67
+ },
68
+ listAnnotations(sessionId) {
69
+ const data = readStore();
70
+ return Object.values(data.annotations)
71
+ .filter((a) => a?.sessionId === sessionId)
72
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
73
+ },
74
+ async addReply(annotationId, reply) {
75
+ let result;
76
+ await withLock((data) => {
77
+ const annotation = data.annotations[annotationId];
78
+ if (!annotation)
79
+ return data;
80
+ annotation.replies.push({ id: (0, node_crypto_1.randomUUID)(), createdAt: new Date().toISOString(), ...reply });
81
+ result = annotation;
82
+ return data;
83
+ });
84
+ return result;
85
+ },
86
+ };
87
+ }
88
+ function buildManifest(projectRoot) {
89
+ const manifest = {};
90
+ const srcDir = node_path_1.default.join(projectRoot, 'src');
91
+ if (!node_fs_1.default.existsSync(srcDir))
92
+ return manifest;
93
+ function scan(dir) {
94
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
95
+ const fullPath = node_path_1.default.join(dir, entry.name);
96
+ if (entry.isDirectory()) {
97
+ scan(fullPath);
98
+ continue;
99
+ }
100
+ if (!entry.name.endsWith('.ts') || entry.name.endsWith('.spec.ts'))
101
+ continue;
102
+ const code = node_fs_1.default.readFileSync(fullPath, 'utf8');
103
+ if (!code.includes('@Component'))
104
+ continue;
105
+ const classMatch = /export\s+class\s+(\w+)/.exec(code);
106
+ if (!classMatch)
107
+ continue;
108
+ const relPath = node_path_1.default.relative(projectRoot, fullPath).replace(/\\/g, '/');
109
+ const item = { component: relPath };
110
+ const templateMatch = /templateUrl\s*:\s*['"`]([^'"`]+)['"`]/.exec(code);
111
+ if (templateMatch) {
112
+ const templateAbs = node_path_1.default.resolve(node_path_1.default.dirname(fullPath), templateMatch[1]);
113
+ item.template = node_path_1.default.relative(projectRoot, templateAbs).replace(/\\/g, '/');
114
+ }
115
+ manifest[classMatch[1]] = item;
116
+ }
117
+ }
118
+ scan(srcDir);
119
+ return manifest;
120
+ }
121
+ // ─── WebSocket handler ────────────────────────────────────────────────────────
122
+ const SYNC_INTERVAL_MS = 2000;
123
+ function safeSend(ws, data) {
124
+ if (ws.readyState !== ws_1.WebSocket.OPEN)
125
+ return;
126
+ ws.send(JSON.stringify(data), (err) => { if (err) { /* connection closed mid-send */ } });
127
+ }
128
+ function createAnnotateWsHandler(store) {
129
+ const wss = new ws_1.WebSocketServer({ noServer: true });
130
+ const sessionSockets = new Map();
131
+ wss.on('connection', (ws, req) => {
132
+ void (async () => {
133
+ const url = req.headers.referer ?? req.headers.origin ?? '';
134
+ let sessionId;
135
+ try {
136
+ const session = await store.createSession(url);
137
+ sessionId = session.id;
138
+ safeSend(ws, { type: 'session:created', session });
139
+ sessionSockets.set(sessionId, ws);
140
+ }
141
+ catch (err) {
142
+ process.stderr.write(`[ng-annotate] Failed to create session: ${String(err)}\n`);
143
+ return;
144
+ }
145
+ ws.on('message', (raw) => {
146
+ void (async () => {
147
+ let msg;
148
+ try {
149
+ msg = JSON.parse(raw.toString());
150
+ }
151
+ catch {
152
+ return;
153
+ }
154
+ try {
155
+ if (msg.type === 'annotation:create' && msg.payload) {
156
+ const annotation = await store.createAnnotation({ ...msg.payload, sessionId });
157
+ safeSend(ws, { type: 'annotation:created', annotation });
158
+ }
159
+ else if (msg.type === 'annotation:reply' && msg.id && msg.message) {
160
+ const updated = await store.addReply(msg.id, { author: 'user', message: msg.message });
161
+ if (updated)
162
+ safeSend(ws, { type: 'annotation:updated', annotation: updated });
163
+ }
164
+ }
165
+ catch (err) {
166
+ process.stderr.write(`[ng-annotate] Failed to process message: ${String(err)}\n`);
167
+ }
168
+ })();
169
+ });
170
+ ws.on('close', () => {
171
+ void store.updateSession(sessionId, { active: false });
172
+ sessionSockets.delete(sessionId);
173
+ });
174
+ })();
175
+ });
176
+ setInterval(() => {
177
+ for (const [sessionId, ws] of sessionSockets) {
178
+ if (ws.readyState !== ws_1.WebSocket.OPEN)
179
+ continue;
180
+ const annotations = store.listAnnotations(sessionId);
181
+ safeSend(ws, { type: 'annotations:sync', annotations });
182
+ }
183
+ }, SYNC_INTERVAL_MS);
184
+ return { wss };
185
+ }
186
+ // ─── Builder ──────────────────────────────────────────────────────────────────
187
+ exports.default = (0, architect_1.createBuilder)((options, context) => {
188
+ const projectRoot = context.workspaceRoot;
189
+ const store = makeStore(projectRoot);
190
+ const manifest = buildManifest(projectRoot);
191
+ const { wss } = createAnnotateWsHandler(store);
192
+ let wsAttached = false;
193
+ const middleware = (req, _res, next) => {
194
+ if (!wsAttached) {
195
+ wsAttached = true;
196
+ const httpServer = req.socket.server;
197
+ httpServer.on('upgrade', (upgradeReq, socket, head) => {
198
+ if (upgradeReq.url === '/__annotate') {
199
+ wss.handleUpgrade(upgradeReq, socket, head, (ws) => {
200
+ wss.emit('connection', ws, upgradeReq);
201
+ });
202
+ }
203
+ });
204
+ process.stderr.write(`[ng-annotate] WebSocket handler attached to Angular dev server\n`);
205
+ }
206
+ next();
207
+ };
208
+ const indexHtmlTransformer = (content) => {
209
+ const script = `<script>window.__NG_ANNOTATE_MANIFEST__ = ${JSON.stringify(manifest)};</script>`;
210
+ return Promise.resolve(content.replace('</head>', ` ${script}\n</head>`));
211
+ };
212
+ return (0, build_1.executeDevServerBuilder)(options, context, {
213
+ middleware: [middleware],
214
+ indexHtmlTransformer,
215
+ });
216
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "title": "ng-annotate Dev Server",
4
+ "description": "Wraps @angular/build:dev-server with integrated ng-annotate WebSocket and manifest support.",
5
+ "type": "object",
6
+ "properties": {}
7
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@ng-annotate/angular",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "schematics": "./schematics/collection.json",
5
+ "builders": "./builders.json",
5
6
  "description": "Angular library for ng-annotate-mcp — browser overlay for annotating components and routing instructions to an AI agent",
6
7
  "keywords": [
7
8
  "angular",
@@ -19,6 +20,7 @@
19
20
  "files": [
20
21
  "dist",
21
22
  "schematics",
23
+ "builders.json",
22
24
  "README.md"
23
25
  ],
24
26
  "main": "dist/fesm2022/ng-annotate-angular.mjs",
@@ -34,21 +36,30 @@
34
36
  }
35
37
  },
36
38
  "scripts": {
37
- "build": "ng-packagr -p ng-package.json && npm run build:schematics",
39
+ "build": "ng-packagr -p ng-package.json && npm run build:schematics && npm run build:builders",
38
40
  "build:watch": "ng-packagr -p ng-package.json --watch",
39
41
  "build:schematics": "tsc -p schematics/tsconfig.json",
42
+ "build:builders": "tsc -p builders/tsconfig.json && node -e \"require('fs').cpSync('src/builders/dev-server/schema.json','dist/builders/dev-server/schema.json')\"",
40
43
  "test": "vitest run schematics",
41
44
  "test:watch": "vitest schematics",
42
45
  "lint": "eslint src/",
43
46
  "lint:fix": "eslint src/ --fix"
44
47
  },
45
48
  "peerDependencies": {
49
+ "@angular-devkit/architect": ">=0.1700.0",
50
+ "@angular/build": ">=21.0.0",
46
51
  "@angular/core": ">=21.0.0",
47
52
  "rxjs": ">=7.0.0"
48
53
  },
54
+ "dependencies": {
55
+ "ws": "^8.19.0"
56
+ },
49
57
  "devDependencies": {
58
+ "@angular-devkit/architect": ">=0.2100.0",
50
59
  "@angular-devkit/schematics": ">=17.0.0",
60
+ "@angular/build": "^21.0.0",
51
61
  "@angular/core": "^21.0.0",
62
+ "@types/ws": "^8.18.1",
52
63
  "ng-packagr": "^21.2.0",
53
64
  "rxjs": "^7.0.0",
54
65
  "typescript": "~5.9.0",
@@ -40,6 +40,8 @@ const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const helpers_1 = require("./helpers");
42
42
  const MIN_ANGULAR_MAJOR = 21;
43
+ const NG_ANNOTATE_BUILDER = '@ng-annotate/angular:dev-server';
44
+ const ANGULAR_DEV_SERVER_BUILDER = '@angular/build:dev-server';
43
45
  function checkAngularVersion() {
44
46
  return (tree) => {
45
47
  const pkgPath = 'package.json';
@@ -55,18 +57,12 @@ function checkAngularVersion() {
55
57
  }
56
58
  };
57
59
  }
58
- function addProxyConfig() {
60
+ function updateAngularJsonBuilder() {
59
61
  return (tree, context) => {
60
- // Create proxy.conf.json
61
- const proxyPath = 'proxy.conf.json';
62
- if (!tree.exists(proxyPath)) {
63
- tree.create(proxyPath, JSON.stringify({ '/__annotate': { target: 'ws://localhost:4201', ws: true } }, null, 2) + '\n');
64
- context.logger.info('✅ Created proxy.conf.json');
65
- }
66
- // Update angular.json serve options
67
62
  const angularJsonPath = 'angular.json';
68
63
  if (!tree.exists(angularJsonPath)) {
69
- context.logger.warn('⚠️ Could not find angular.json — add proxyConfig manually');
64
+ context.logger.warn('⚠️ Could not find angular.json — update the serve builder manually:\n' +
65
+ ` "builder": "${NG_ANNOTATE_BUILDER}"`);
70
66
  return;
71
67
  }
72
68
  const angularJson = JSON.parse(tree.read(angularJsonPath).toString('utf-8'));
@@ -82,51 +78,26 @@ function addProxyConfig() {
82
78
  const serve = architect['serve'];
83
79
  if (!serve)
84
80
  continue;
85
- const options = (serve['options'] ?? {});
86
- if (options['proxyConfig'])
81
+ const currentBuilder = serve['builder'];
82
+ if (currentBuilder === NG_ANNOTATE_BUILDER) {
83
+ context.logger.info(`ng-annotate builder already configured in ${projectName}, skipping.`);
84
+ continue;
85
+ }
86
+ if (currentBuilder !== ANGULAR_DEV_SERVER_BUILDER) {
87
+ context.logger.warn(`⚠️ Project "${projectName}" uses builder "${String(currentBuilder)}" which is not ` +
88
+ `"${ANGULAR_DEV_SERVER_BUILDER}". Skipping automatic builder update — ` +
89
+ `set it to "${NG_ANNOTATE_BUILDER}" manually if compatible.`);
87
90
  continue;
88
- serve['options'] = { ...options, proxyConfig: 'proxy.conf.json' };
91
+ }
92
+ serve['builder'] = NG_ANNOTATE_BUILDER;
89
93
  changed = true;
90
- context.logger.info(`✅ Added proxyConfig to angular.json (${projectName})`);
94
+ context.logger.info(`✅ Updated angular.json serve builder for "${projectName}"`);
91
95
  }
92
96
  if (changed) {
93
97
  tree.overwrite(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
94
98
  }
95
99
  };
96
100
  }
97
- function addVitePlugin() {
98
- return (tree, context) => {
99
- // Angular CLI does not load vite.config.ts plugins — skip for Angular projects
100
- if (tree.exists('angular.json')) {
101
- context.logger.info('Angular project detected — skipping vite.config.ts setup (using proxy instead).');
102
- return;
103
- }
104
- const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
105
- const viteConfigPath = candidates.find((p) => tree.exists(p));
106
- if (!viteConfigPath) {
107
- const created = `import { defineConfig } from 'vite';\nimport { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n\nexport default defineConfig({\n plugins: [...ngAnnotateMcp()],\n});\n`;
108
- tree.create('vite.config.ts', created);
109
- context.logger.info('✅ Created vite.config.ts with ngAnnotateMcp()');
110
- return;
111
- }
112
- let content = tree.read(viteConfigPath).toString('utf-8');
113
- if (content.includes('@ng-annotate/vite-plugin')) {
114
- context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
115
- return;
116
- }
117
- content = (0, helpers_1.insertAfterLastImport)(content, "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';");
118
- if (/plugins\s*:\s*\[/.test(content)) {
119
- // Existing plugins array — prepend into it
120
- content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
121
- }
122
- else {
123
- // No plugins array — inject one into defineConfig({...})
124
- content = content.replace(/defineConfig\(\s*\{/, 'defineConfig({\n plugins: [...ngAnnotateMcp()],');
125
- }
126
- tree.overwrite(viteConfigPath, content);
127
- context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
128
- };
129
- }
130
101
  function addProviders() {
131
102
  return (tree, context) => {
132
103
  const candidates = [
@@ -268,12 +239,17 @@ function addDevDependency() {
268
239
  if (!tree.exists(pkgPath))
269
240
  return;
270
241
  const pkg = JSON.parse(tree.read(pkgPath).toString('utf-8'));
242
+ pkg['dependencies'] ?? (pkg['dependencies'] = {});
271
243
  pkg['devDependencies'] ?? (pkg['devDependencies'] = {});
272
244
  let changed = false;
273
- if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
274
- pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
245
+ // ng add installs @ng-annotate/angular into dependencies by default.
246
+ // Move it to devDependencies — it is a dev-only tool.
247
+ const ngAnnotateVersion = pkg['dependencies']['@ng-annotate/angular'];
248
+ if (ngAnnotateVersion && !pkg['devDependencies']['@ng-annotate/angular']) {
249
+ pkg['devDependencies']['@ng-annotate/angular'] = ngAnnotateVersion;
250
+ delete pkg['dependencies']['@ng-annotate/angular'];
275
251
  changed = true;
276
- context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
252
+ context.logger.info('✅ Moved @ng-annotate/angular to devDependencies');
277
253
  }
278
254
  if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
279
255
  pkg['devDependencies']['@ng-annotate/mcp-server'] = 'latest';
@@ -310,8 +286,7 @@ function default_1(options) {
310
286
  return (0, schematics_1.chain)([
311
287
  checkAngularVersion(),
312
288
  addDevDependency(),
313
- addProxyConfig(),
314
- addVitePlugin(),
289
+ updateAngularJsonBuilder(),
315
290
  addProviders(),
316
291
  addMcpConfig(options),
317
292
  addGitignore(),
@@ -72,8 +72,21 @@ const BASE_PKG = JSON.stringify({
72
72
  devDependencies: {},
73
73
  });
74
74
 
75
- async function runSchematic(tree: UnitTestTree): Promise<UnitTestTree> {
76
- return runner.runSchematic('ng-add', { aiTool: 'claude-code' }, tree);
75
+ const ANGULAR_JSON = JSON.stringify({
76
+ projects: {
77
+ 'my-app': {
78
+ architect: {
79
+ serve: {
80
+ builder: '@angular/build:dev-server',
81
+ options: {},
82
+ },
83
+ },
84
+ },
85
+ },
86
+ });
87
+
88
+ async function runSchematic(tree: Tree): Promise<ReturnType<typeof Tree.empty> & { readText(p: string): string }> {
89
+ return runner.runSchematic('ng-add', { aiTool: 'claude-code' }, tree) as Promise<ReturnType<typeof Tree.empty> & { readText(p: string): string }>;
77
90
  }
78
91
 
79
92
  function makeTree(files: Record<string, string>): Tree {
@@ -84,51 +97,110 @@ function makeTree(files: Record<string, string>): Tree {
84
97
  return tree;
85
98
  }
86
99
 
87
- describe('ng-add schematic addVitePlugin', () => {
88
- it('creates vite.config.ts when none exists', async () => {
100
+ // ─── addDevDependency ─────────────────────────────────────────────────────────
101
+
102
+ describe('ng-add schematic — addDevDependency', () => {
103
+ it('moves @ng-annotate/angular from dependencies to devDependencies', async () => {
104
+ const pkg = JSON.stringify({
105
+ dependencies: { '@angular/core': '^21.0.0', '@ng-annotate/angular': '^0.5.0' },
106
+ devDependencies: {},
107
+ });
108
+ const tree = makeTree({ 'package.json': pkg });
109
+ const result = await runSchematic(tree);
110
+ const parsed = JSON.parse(result.readText('package.json')) as Record<string, Record<string, string>>;
111
+ expect(parsed['devDependencies']['@ng-annotate/angular']).toBe('^0.5.0');
112
+ expect(parsed['dependencies']['@ng-annotate/angular']).toBeUndefined();
113
+ });
114
+
115
+ it('adds @ng-annotate/mcp-server to devDependencies', async () => {
89
116
  const tree = makeTree({ 'package.json': BASE_PKG });
90
117
  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()]');
118
+ const parsed = JSON.parse(result.readText('package.json')) as Record<string, Record<string, string>>;
119
+ expect(parsed['devDependencies']['@ng-annotate/mcp-server']).toBe('latest');
95
120
  });
96
121
 
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`,
122
+ it('skips @ng-annotate/mcp-server if already present', async () => {
123
+ const pkg = JSON.stringify({
124
+ dependencies: { '@angular/core': '^21.0.0' },
125
+ devDependencies: { '@ng-annotate/mcp-server': '^0.4.0' },
101
126
  });
127
+ const tree = makeTree({ 'package.json': pkg });
102
128
  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(), ');
129
+ const parsed = JSON.parse(result.readText('package.json')) as Record<string, Record<string, string>>;
130
+ expect(parsed['devDependencies']['@ng-annotate/mcp-server']).toBe('^0.4.0');
106
131
  });
132
+ });
107
133
 
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`,
134
+ // ─── updateAngularJsonBuilder ─────────────────────────────────────────────────
135
+
136
+ describe('ng-add schematic — updateAngularJsonBuilder', () => {
137
+ it('updates builder to @ng-annotate/angular:dev-server', async () => {
138
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
139
+ const result = await runSchematic(tree);
140
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
141
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
142
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
143
+ expect((serve['builder'] as string)).toBe('@ng-annotate/angular:dev-server');
144
+ });
145
+
146
+ it('skips when already using ng-annotate builder', async () => {
147
+ const withNgAnnotate = JSON.stringify({
148
+ projects: {
149
+ 'my-app': {
150
+ architect: {
151
+ serve: {
152
+ builder: '@ng-annotate/angular:dev-server',
153
+ options: {},
154
+ },
155
+ },
156
+ },
157
+ },
112
158
  });
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`,
159
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': withNgAnnotate });
160
+ const result = await runSchematic(tree);
161
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
162
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
163
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
164
+ expect((serve['builder'] as string)).toBe('@ng-annotate/angular:dev-server');
165
+ });
166
+
167
+ it('warns but does not change unknown builders', async () => {
168
+ const withCustomBuilder = JSON.stringify({
169
+ projects: {
170
+ 'my-app': {
171
+ architect: {
172
+ serve: {
173
+ builder: '@custom/builder:dev-server',
174
+ options: {},
175
+ },
176
+ },
177
+ },
178
+ },
118
179
  });
119
- const result = await runSchematic(tree2);
120
- const content = result.readText('vite.config.ts');
121
- expect(content).toContain('plugins: [...ngAnnotateMcp()]');
180
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': withCustomBuilder });
181
+ const result = await runSchematic(tree);
182
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
183
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
184
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
185
+ // Should NOT overwrite a custom builder
186
+ expect((serve['builder'] as string)).toBe('@custom/builder:dev-server');
187
+ });
188
+
189
+ it('does not create vite.config.ts (Angular CLI never loads it)', async () => {
190
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
191
+ const result = await runSchematic(tree);
192
+ expect(result.exists('vite.config.ts')).toBe(false);
122
193
  });
123
194
 
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 });
195
+ it('does not create proxy.conf.json (builder handles WS internally)', async () => {
196
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
127
197
  const result = await runSchematic(tree);
128
- expect(result.readText('vite.config.ts')).toBe(original);
198
+ expect(result.exists('proxy.conf.json')).toBe(false);
129
199
  });
130
200
  });
131
201
 
202
+ // ─── addProviders ─────────────────────────────────────────────────────────────
203
+
132
204
  describe('ng-add schematic — addProviders', () => {
133
205
  it('adds import and provideNgAnnotate() to standard app.config.ts', async () => {
134
206
  const tree = makeTree({
@@ -162,71 +234,7 @@ describe('ng-add schematic — addProviders', () => {
162
234
  });
163
235
  });
164
236
 
165
- describe('ng-add schematic addProxyConfig', () => {
166
- const ANGULAR_JSON = JSON.stringify({
167
- projects: {
168
- 'my-app': {
169
- architect: {
170
- serve: {
171
- builder: '@angular/build:dev-server',
172
- options: {},
173
- },
174
- },
175
- },
176
- },
177
- });
178
-
179
- it('creates proxy.conf.json and updates angular.json', async () => {
180
- const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
181
- const result = await runSchematic(tree);
182
- expect(result.exists('proxy.conf.json')).toBe(true);
183
- expect(result.readText('proxy.conf.json')).toContain('/__annotate');
184
- const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
185
- const projects = angular['projects'] as Record<string, Record<string, unknown>>;
186
- const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
187
- expect((serve['options'] as Record<string, unknown>)['proxyConfig']).toBe('proxy.conf.json');
188
- });
189
-
190
- it('skips proxy.conf.json if already exists', async () => {
191
- const original = '{"/__annotate":{"target":"ws://localhost:4201","ws":true}}\n';
192
- const tree = makeTree({
193
- 'package.json': BASE_PKG,
194
- 'angular.json': ANGULAR_JSON,
195
- 'proxy.conf.json': original,
196
- });
197
- const result = await runSchematic(tree);
198
- expect(result.readText('proxy.conf.json')).toBe(original);
199
- });
200
-
201
- it('skips angular.json proxyConfig if already set', async () => {
202
- const withProxy = JSON.stringify({
203
- projects: {
204
- 'my-app': {
205
- architect: {
206
- serve: {
207
- builder: '@angular/build:dev-server',
208
- options: { proxyConfig: 'proxy.conf.json' },
209
- },
210
- },
211
- },
212
- },
213
- });
214
- const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': withProxy });
215
- const result = await runSchematic(tree);
216
- // Content should be unchanged (same proxyConfig value)
217
- const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
218
- const projects = angular['projects'] as Record<string, Record<string, unknown>>;
219
- const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
220
- expect((serve['options'] as Record<string, unknown>)['proxyConfig']).toBe('proxy.conf.json');
221
- });
222
-
223
- it('skips vite.config.ts setup when angular.json present', async () => {
224
- const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
225
- const result = await runSchematic(tree);
226
- // Should NOT create vite.config.ts (Angular doesn't load vite plugins)
227
- expect(result.exists('vite.config.ts')).toBe(false);
228
- });
229
- });
237
+ // ─── addGitignore ─────────────────────────────────────────────────────────────
230
238
 
231
239
  describe('ng-add schematic — addGitignore', () => {
232
240
  it('creates .gitignore with .ng-annotate/ when none exists', async () => {
@@ -9,6 +9,8 @@ interface Options {
9
9
  }
10
10
 
11
11
  const MIN_ANGULAR_MAJOR = 21;
12
+ const NG_ANNOTATE_BUILDER = '@ng-annotate/angular:dev-server';
13
+ const ANGULAR_DEV_SERVER_BUILDER = '@angular/build:dev-server';
12
14
 
13
15
  function checkAngularVersion(): Rule {
14
16
  return (tree: Tree) => {
@@ -33,22 +35,14 @@ function checkAngularVersion(): Rule {
33
35
  };
34
36
  }
35
37
 
36
- function addProxyConfig(): Rule {
38
+ function updateAngularJsonBuilder(): Rule {
37
39
  return (tree: Tree, context: SchematicContext) => {
38
- // Create proxy.conf.json
39
- const proxyPath = 'proxy.conf.json';
40
- if (!tree.exists(proxyPath)) {
41
- tree.create(
42
- proxyPath,
43
- JSON.stringify({ '/__annotate': { target: 'ws://localhost:4201', ws: true } }, null, 2) + '\n',
44
- );
45
- context.logger.info('✅ Created proxy.conf.json');
46
- }
47
-
48
- // Update angular.json serve options
49
40
  const angularJsonPath = 'angular.json';
50
41
  if (!tree.exists(angularJsonPath)) {
51
- context.logger.warn('⚠️ Could not find angular.json — add proxyConfig manually');
42
+ context.logger.warn(
43
+ '⚠️ Could not find angular.json — update the serve builder manually:\n' +
44
+ ` "builder": "${NG_ANNOTATE_BUILDER}"`,
45
+ );
52
46
  return;
53
47
  }
54
48
 
@@ -68,12 +62,25 @@ function addProxyConfig(): Rule {
68
62
  const serve = architect['serve'] as Record<string, unknown> | undefined;
69
63
  if (!serve) continue;
70
64
 
71
- const options = (serve['options'] ?? {}) as Record<string, unknown>;
72
- if (options['proxyConfig']) continue;
65
+ const currentBuilder = serve['builder'] as string | undefined;
66
+
67
+ if (currentBuilder === NG_ANNOTATE_BUILDER) {
68
+ context.logger.info(`ng-annotate builder already configured in ${projectName}, skipping.`);
69
+ continue;
70
+ }
71
+
72
+ if (currentBuilder !== ANGULAR_DEV_SERVER_BUILDER) {
73
+ context.logger.warn(
74
+ `⚠️ Project "${projectName}" uses builder "${String(currentBuilder)}" which is not ` +
75
+ `"${ANGULAR_DEV_SERVER_BUILDER}". Skipping automatic builder update — ` +
76
+ `set it to "${NG_ANNOTATE_BUILDER}" manually if compatible.`,
77
+ );
78
+ continue;
79
+ }
73
80
 
74
- serve['options'] = { ...options, proxyConfig: 'proxy.conf.json' };
81
+ serve['builder'] = NG_ANNOTATE_BUILDER;
75
82
  changed = true;
76
- context.logger.info(`✅ Added proxyConfig to angular.json (${projectName})`);
83
+ context.logger.info(`✅ Updated angular.json serve builder for "${projectName}"`);
77
84
  }
78
85
 
79
86
  if (changed) {
@@ -82,46 +89,6 @@ function addProxyConfig(): Rule {
82
89
  };
83
90
  }
84
91
 
85
- function addVitePlugin(): Rule {
86
- return (tree: Tree, context: SchematicContext) => {
87
- // Angular CLI does not load vite.config.ts plugins — skip for Angular projects
88
- if (tree.exists('angular.json')) {
89
- context.logger.info('Angular project detected — skipping vite.config.ts setup (using proxy instead).');
90
- return;
91
- }
92
-
93
- const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
94
- const viteConfigPath = candidates.find((p) => tree.exists(p));
95
-
96
- if (!viteConfigPath) {
97
- const created = `import { defineConfig } from 'vite';\nimport { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n\nexport default defineConfig({\n plugins: [...ngAnnotateMcp()],\n});\n`;
98
- tree.create('vite.config.ts', created);
99
- context.logger.info('✅ Created vite.config.ts with ngAnnotateMcp()');
100
- return;
101
- }
102
-
103
- let content = tree.read(viteConfigPath)!.toString('utf-8');
104
-
105
- if (content.includes('@ng-annotate/vite-plugin')) {
106
- context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
107
- return;
108
- }
109
-
110
- content = insertAfterLastImport(content, "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';");
111
-
112
- if (/plugins\s*:\s*\[/.test(content)) {
113
- // Existing plugins array — prepend into it
114
- content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
115
- } else {
116
- // No plugins array — inject one into defineConfig({...})
117
- content = content.replace(/defineConfig\(\s*\{/, 'defineConfig({\n plugins: [...ngAnnotateMcp()],');
118
- }
119
-
120
- tree.overwrite(viteConfigPath, content);
121
- context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
122
- };
123
- }
124
-
125
92
  function addProviders(): Rule {
126
93
  return (tree: Tree, context: SchematicContext) => {
127
94
  const candidates = [
@@ -301,14 +268,19 @@ function addDevDependency(): Rule {
301
268
  string,
302
269
  Record<string, string>
303
270
  >;
271
+ pkg['dependencies'] ??= {};
304
272
  pkg['devDependencies'] ??= {};
305
273
 
306
274
  let changed = false;
307
275
 
308
- if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
309
- pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
276
+ // ng add installs @ng-annotate/angular into dependencies by default.
277
+ // Move it to devDependencies — it is a dev-only tool.
278
+ const ngAnnotateVersion = pkg['dependencies']['@ng-annotate/angular'];
279
+ if (ngAnnotateVersion && !pkg['devDependencies']['@ng-annotate/angular']) {
280
+ pkg['devDependencies']['@ng-annotate/angular'] = ngAnnotateVersion;
281
+ delete pkg['dependencies']['@ng-annotate/angular'];
310
282
  changed = true;
311
- context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
283
+ context.logger.info('✅ Moved @ng-annotate/angular to devDependencies');
312
284
  }
313
285
 
314
286
  if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
@@ -352,8 +324,7 @@ export default function (options: Options): Rule {
352
324
  return chain([
353
325
  checkAngularVersion(),
354
326
  addDevDependency(),
355
- addProxyConfig(),
356
- addVitePlugin(),
327
+ updateAngularJsonBuilder(),
357
328
  addProviders(),
358
329
  addMcpConfig(options),
359
330
  addGitignore(),