@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 +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 +26 -51
- package/schematics/ng-add/index.spec.ts +104 -96
- package/schematics/ng-add/index.ts +33 -62
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.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
|
|
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 = [
|
|
@@ -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
|
-
|
|
274
|
-
|
|
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('✅
|
|
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
|
-
|
|
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
|
-
|
|
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,110 @@ function makeTree(files: Record<string, string>): Tree {
|
|
|
84
97
|
return tree;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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('
|
|
98
|
-
const
|
|
99
|
-
'
|
|
100
|
-
|
|
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
|
|
104
|
-
expect(
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
120
|
-
const
|
|
121
|
-
|
|
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('
|
|
125
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
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['
|
|
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 = [
|
|
@@ -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
|
-
|
|
309
|
-
|
|
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('✅
|
|
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
|
-
|
|
356
|
-
addVitePlugin(),
|
|
327
|
+
updateAngularJsonBuilder(),
|
|
357
328
|
addProviders(),
|
|
358
329
|
addMcpConfig(options),
|
|
359
330
|
addGitignore(),
|