@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 +10 -0
- package/dist/builders/dev-server/index.d.ts +3 -0
- package/dist/builders/dev-server/index.js +216 -0
- package/dist/builders/dev-server/schema.json +7 -0
- package/package.json +13 -2
- package/schematics/ng-add/index.js +18 -53
- package/schematics/ng-add/index.spec.ts +72 -98
- package/schematics/ng-add/index.ts +25 -65
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,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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-annotate/angular",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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 —
|
|
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
|
|
86
|
-
if (
|
|
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
|
-
|
|
91
|
+
}
|
|
92
|
+
serve['builder'] = NG_ANNOTATE_BUILDER;
|
|
89
93
|
changed = true;
|
|
90
|
-
context.logger.info(`✅
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
expect(
|
|
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('
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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('
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
120
|
-
const
|
|
121
|
-
|
|
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('
|
|
125
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
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(`✅
|
|
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
|
-
|
|
356
|
-
addVitePlugin(),
|
|
316
|
+
updateAngularJsonBuilder(),
|
|
357
317
|
addProviders(),
|
|
358
318
|
addMcpConfig(options),
|
|
359
319
|
addGitignore(),
|