@ng-annotate/angular 0.4.1 → 0.5.0

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.0",
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 = [
@@ -270,11 +241,6 @@ function addDevDependency() {
270
241
  const pkg = JSON.parse(tree.read(pkgPath).toString('utf-8'));
271
242
  pkg['devDependencies'] ?? (pkg['devDependencies'] = {});
272
243
  let changed = false;
273
- if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
274
- pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
275
- changed = true;
276
- context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
277
- }
278
244
  if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
279
245
  pkg['devDependencies']['@ng-annotate/mcp-server'] = 'latest';
280
246
  changed = true;
@@ -310,8 +276,7 @@ function default_1(options) {
310
276
  return (0, schematics_1.chain)([
311
277
  checkAngularVersion(),
312
278
  addDevDependency(),
313
- addProxyConfig(),
314
- addVitePlugin(),
279
+ updateAngularJsonBuilder(),
315
280
  addProviders(),
316
281
  addMcpConfig(options),
317
282
  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,76 @@ 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 () => {
89
- const tree = makeTree({ 'package.json': BASE_PKG });
100
+ // ─── updateAngularJsonBuilder ─────────────────────────────────────────────────
101
+
102
+ describe('ng-add schematic updateAngularJsonBuilder', () => {
103
+ it('updates builder to @ng-annotate/angular:dev-server', async () => {
104
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
90
105
  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()]');
106
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
107
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
108
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
109
+ expect((serve['builder'] as string)).toBe('@ng-annotate/angular:dev-server');
95
110
  });
96
111
 
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`,
112
+ it('skips when already using ng-annotate builder', async () => {
113
+ const withNgAnnotate = JSON.stringify({
114
+ projects: {
115
+ 'my-app': {
116
+ architect: {
117
+ serve: {
118
+ builder: '@ng-annotate/angular:dev-server',
119
+ options: {},
120
+ },
121
+ },
122
+ },
123
+ },
101
124
  });
125
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': withNgAnnotate });
102
126
  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(), ');
127
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
128
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
129
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
130
+ expect((serve['builder'] as string)).toBe('@ng-annotate/angular:dev-server');
106
131
  });
107
132
 
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`,
133
+ it('warns but does not change unknown builders', async () => {
134
+ const withCustomBuilder = JSON.stringify({
135
+ projects: {
136
+ 'my-app': {
137
+ architect: {
138
+ serve: {
139
+ builder: '@custom/builder:dev-server',
140
+ options: {},
141
+ },
142
+ },
143
+ },
144
+ },
118
145
  });
119
- const result = await runSchematic(tree2);
120
- const content = result.readText('vite.config.ts');
121
- expect(content).toContain('plugins: [...ngAnnotateMcp()]');
146
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': withCustomBuilder });
147
+ const result = await runSchematic(tree);
148
+ const angular = JSON.parse(result.readText('angular.json')) as Record<string, unknown>;
149
+ const projects = angular['projects'] as Record<string, Record<string, unknown>>;
150
+ const serve = (projects['my-app']['architect'] as Record<string, Record<string, unknown>>)['serve'];
151
+ // Should NOT overwrite a custom builder
152
+ expect((serve['builder'] as string)).toBe('@custom/builder:dev-server');
153
+ });
154
+
155
+ it('does not create vite.config.ts (Angular CLI never loads it)', async () => {
156
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
157
+ const result = await runSchematic(tree);
158
+ expect(result.exists('vite.config.ts')).toBe(false);
122
159
  });
123
160
 
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 });
161
+ it('does not create proxy.conf.json (builder handles WS internally)', async () => {
162
+ const tree = makeTree({ 'package.json': BASE_PKG, 'angular.json': ANGULAR_JSON });
127
163
  const result = await runSchematic(tree);
128
- expect(result.readText('vite.config.ts')).toBe(original);
164
+ expect(result.exists('proxy.conf.json')).toBe(false);
129
165
  });
130
166
  });
131
167
 
168
+ // ─── addProviders ─────────────────────────────────────────────────────────────
169
+
132
170
  describe('ng-add schematic — addProviders', () => {
133
171
  it('adds import and provideNgAnnotate() to standard app.config.ts', async () => {
134
172
  const tree = makeTree({
@@ -162,71 +200,7 @@ describe('ng-add schematic — addProviders', () => {
162
200
  });
163
201
  });
164
202
 
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
- });
203
+ // ─── addGitignore ─────────────────────────────────────────────────────────────
230
204
 
231
205
  describe('ng-add schematic — addGitignore', () => {
232
206
  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
+ }
73
71
 
74
- serve['options'] = { ...options, proxyConfig: 'proxy.conf.json' };
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
+ }
80
+
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 = [
@@ -305,12 +272,6 @@ function addDevDependency(): Rule {
305
272
 
306
273
  let changed = false;
307
274
 
308
- if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
309
- pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
310
- changed = true;
311
- context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
312
- }
313
-
314
275
  if (!pkg['devDependencies']['@ng-annotate/mcp-server']) {
315
276
  pkg['devDependencies']['@ng-annotate/mcp-server'] = 'latest';
316
277
  changed = true;
@@ -352,8 +313,7 @@ export default function (options: Options): Rule {
352
313
  return chain([
353
314
  checkAngularVersion(),
354
315
  addDevDependency(),
355
- addProxyConfig(),
356
- addVitePlugin(),
316
+ updateAngularJsonBuilder(),
357
317
  addProviders(),
358
318
  addMcpConfig(options),
359
319
  addGitignore(),