@supacloud/compiler 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/README.md +29 -1
- package/dist/analyze.d.ts +2 -2
- package/dist/cli.js +2343 -148
- package/dist/generate.d.ts +15 -0
- package/dist/incremental.d.ts +30 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +2180 -92
- package/dist/inspect.d.ts +21 -0
- package/dist/types.d.ts +133 -0
- package/dist/util.d.ts +4 -0
- package/dist/validate.d.ts +4 -0
- package/dist/watch.d.ts +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/analyze.ts
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { join, relative, sep } from "node:path";
|
|
4
5
|
import {
|
|
5
6
|
Node,
|
|
@@ -17,10 +18,20 @@ var ROUTE_DECORATORS = {
|
|
|
17
18
|
Options: "OPTIONS"
|
|
18
19
|
};
|
|
19
20
|
var SCOPES = ["application", "request", "job"];
|
|
20
|
-
async function analyzeProject(rootDir, include) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
async function analyzeProject(rootDir, include, cache) {
|
|
22
|
+
let project;
|
|
23
|
+
if (cache?.project) {
|
|
24
|
+
project = cache.project;
|
|
25
|
+
for (const sf of project.getSourceFiles()) {
|
|
26
|
+
sf.refreshFromFileSystemSync();
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
project = createProject(rootDir);
|
|
30
|
+
const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
|
|
31
|
+
project.addSourceFilesAtPaths(patterns);
|
|
32
|
+
if (cache)
|
|
33
|
+
cache.project = project;
|
|
34
|
+
}
|
|
24
35
|
const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
|
|
25
36
|
const ctx = {
|
|
26
37
|
rootDir,
|
|
@@ -70,7 +81,207 @@ async function analyzeProject(rootDir, include) {
|
|
|
70
81
|
for (const c of candidates) {
|
|
71
82
|
nameByNode.set(c.node, stringLiteralProp(c.options, "name") ?? c.className);
|
|
72
83
|
}
|
|
73
|
-
|
|
84
|
+
let modules = [];
|
|
85
|
+
let reusedModules = [];
|
|
86
|
+
let reanalyzedModules = [];
|
|
87
|
+
if (cache) {
|
|
88
|
+
const currentFileHashes = new Map;
|
|
89
|
+
for (const sf of sourceFiles) {
|
|
90
|
+
const rel = sourcePath(rootDir, sf.getFilePath());
|
|
91
|
+
const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
|
|
92
|
+
currentFileHashes.set(rel, hash);
|
|
93
|
+
}
|
|
94
|
+
const changedFiles = new Set;
|
|
95
|
+
for (const [file, hash] of currentFileHashes.entries()) {
|
|
96
|
+
if (cache.fileHashes.get(file) !== hash) {
|
|
97
|
+
changedFiles.add(file);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const file of cache.fileHashes.keys()) {
|
|
101
|
+
if (!currentFileHashes.has(file)) {
|
|
102
|
+
changedFiles.add(file);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const modulesToKeep = new Map;
|
|
106
|
+
const finalModules = [];
|
|
107
|
+
const finalDiagnostics = [];
|
|
108
|
+
const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
|
|
109
|
+
for (const [modName, entry] of cache.modules.entries()) {
|
|
110
|
+
const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
|
|
111
|
+
const moduleFileExists = currentFileHashes.has(entry.module.file);
|
|
112
|
+
const isAffectedByDep = affectedModuleNames.has(modName);
|
|
113
|
+
if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
|
|
114
|
+
modulesToKeep.set(modName, entry);
|
|
115
|
+
reusedModules.push(modName);
|
|
116
|
+
finalModules.push(entry.module);
|
|
117
|
+
if (entry.diagnostics)
|
|
118
|
+
finalDiagnostics.push(...entry.diagnostics);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const c of candidates) {
|
|
122
|
+
const modName = nameByNode.get(c.node) ?? c.className;
|
|
123
|
+
if (modulesToKeep.has(modName)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const diagBefore = ctx.diagnostics.length;
|
|
127
|
+
const parsed = parseModule(c, nameByNode, ctx);
|
|
128
|
+
const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
|
|
129
|
+
const ownedFiles = new Set;
|
|
130
|
+
ownedFiles.add(parsed.file);
|
|
131
|
+
for (const p of parsed.providers)
|
|
132
|
+
if (p.file)
|
|
133
|
+
ownedFiles.add(p.file);
|
|
134
|
+
for (const ctrl of parsed.controllers)
|
|
135
|
+
if (ctrl.file)
|
|
136
|
+
ownedFiles.add(ctrl.file);
|
|
137
|
+
const fileHashes = {};
|
|
138
|
+
for (const f of ownedFiles) {
|
|
139
|
+
fileHashes[f] = currentFileHashes.get(f) ?? "";
|
|
140
|
+
}
|
|
141
|
+
cache.modules.set(parsed.name, {
|
|
142
|
+
module: parsed,
|
|
143
|
+
ownedFiles: [...ownedFiles],
|
|
144
|
+
fileHashes,
|
|
145
|
+
diagnostics: moduleDiagnostics
|
|
146
|
+
});
|
|
147
|
+
reanalyzedModules.push(parsed.name);
|
|
148
|
+
finalModules.push(parsed);
|
|
149
|
+
finalDiagnostics.push(...moduleDiagnostics);
|
|
150
|
+
}
|
|
151
|
+
for (const modName of [...cache.modules.keys()]) {
|
|
152
|
+
if (!modulesToKeep.has(modName) && !reanalyzedModules.includes(modName)) {
|
|
153
|
+
cache.modules.delete(modName);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
cache.fileHashes = currentFileHashes;
|
|
157
|
+
cache.lastStats = { reusedModules, reanalyzedModules };
|
|
158
|
+
ctx.diagnostics = finalDiagnostics;
|
|
159
|
+
modules = finalModules;
|
|
160
|
+
} else {
|
|
161
|
+
modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
|
|
162
|
+
}
|
|
163
|
+
const allRegisteredClasses = new Set;
|
|
164
|
+
const allRegisteredControllers = new Set;
|
|
165
|
+
const allRegisteredCommands = new Set;
|
|
166
|
+
for (const m of modules) {
|
|
167
|
+
for (const p of m.providers) {
|
|
168
|
+
if (p.useClass)
|
|
169
|
+
allRegisteredClasses.add(p.useClass);
|
|
170
|
+
if (p.kind === "class")
|
|
171
|
+
allRegisteredClasses.add(p.token);
|
|
172
|
+
}
|
|
173
|
+
for (const c of m.controllers) {
|
|
174
|
+
allRegisteredControllers.add(c.className);
|
|
175
|
+
}
|
|
176
|
+
for (const cmd of m.commands) {
|
|
177
|
+
allRegisteredCommands.add(cmd.className);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const rootProviders = [];
|
|
181
|
+
const standaloneControllers = [];
|
|
182
|
+
const standaloneCommands = [];
|
|
183
|
+
for (const [name, classInfo] of ctx.classesByName.entries()) {
|
|
184
|
+
if (!allRegisteredClasses.has(name)) {
|
|
185
|
+
const injectable = parseInjectableOptions(classInfo.decl, ctx);
|
|
186
|
+
if (injectable?.providedIn === "root") {
|
|
187
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
|
|
188
|
+
const file = sourcePath(ctx.rootDir, classInfo.file);
|
|
189
|
+
const line = classInfo.decl.getStartLineNumber();
|
|
190
|
+
if (missing) {
|
|
191
|
+
warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
|
|
192
|
+
}
|
|
193
|
+
rootProviders.push({
|
|
194
|
+
token: name,
|
|
195
|
+
tokenKind: "class",
|
|
196
|
+
kind: "class",
|
|
197
|
+
useClass: name,
|
|
198
|
+
scope: injectable.scope ?? "application",
|
|
199
|
+
deps,
|
|
200
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
201
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
202
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
203
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
204
|
+
providedIn: "root",
|
|
205
|
+
hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
|
|
206
|
+
exported: true,
|
|
207
|
+
file,
|
|
208
|
+
line,
|
|
209
|
+
importPath: modulePath(ctx.rootDir, classInfo.file)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!allRegisteredControllers.has(name)) {
|
|
214
|
+
const controllerDec = findDecorator(classInfo.decl, "Controller");
|
|
215
|
+
if (controllerDec) {
|
|
216
|
+
const arg = controllerDec.getArguments()[0];
|
|
217
|
+
const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
|
|
218
|
+
if (isStandalone) {
|
|
219
|
+
const ctrl = parseController(classInfo.decl, ctx);
|
|
220
|
+
if (ctrl)
|
|
221
|
+
standaloneControllers.push(ctrl);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!allRegisteredCommands.has(name)) {
|
|
226
|
+
const commandDec = findDecorator(classInfo.decl, "Command");
|
|
227
|
+
if (commandDec) {
|
|
228
|
+
const meta = decoratorObjectArg(commandDec);
|
|
229
|
+
if (meta && booleanProp(meta, "standalone")) {
|
|
230
|
+
standaloneCommands.push({
|
|
231
|
+
className: classInfo.decl.getName() ?? name,
|
|
232
|
+
name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
|
|
233
|
+
permission: stringLiteralProp(meta, "permission"),
|
|
234
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
235
|
+
audit: stringLiteralProp(meta, "audit"),
|
|
236
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
237
|
+
standalone: true
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const [name, tokenInfo] of ctx.tokensByName.entries()) {
|
|
244
|
+
if (tokenInfo.providedIn === "root" && !allRegisteredClasses.has(name)) {
|
|
245
|
+
rootProviders.push({
|
|
246
|
+
token: name,
|
|
247
|
+
tokenKind: "injection-token",
|
|
248
|
+
kind: "factory",
|
|
249
|
+
scope: tokenInfo.scope ?? "application",
|
|
250
|
+
deps: [],
|
|
251
|
+
providedIn: "root",
|
|
252
|
+
exported: true,
|
|
253
|
+
file: sourcePath(ctx.rootDir, tokenInfo.file),
|
|
254
|
+
line: tokenInfo.line ?? 1,
|
|
255
|
+
importPath: modulePath(ctx.rootDir, tokenInfo.file)
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
|
|
260
|
+
const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
|
|
261
|
+
if (existingRoot) {
|
|
262
|
+
existingRoot.providers.push(...rootProviders);
|
|
263
|
+
existingRoot.controllers.push(...standaloneControllers);
|
|
264
|
+
existingRoot.commands.push(...standaloneCommands);
|
|
265
|
+
for (const p of rootProviders) {
|
|
266
|
+
if (!existingRoot.exports.includes(p.token))
|
|
267
|
+
existingRoot.exports.push(p.token);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
|
|
271
|
+
modules.unshift({
|
|
272
|
+
name: "root",
|
|
273
|
+
className: "RootModule",
|
|
274
|
+
file: fallbackFile,
|
|
275
|
+
line: 1,
|
|
276
|
+
imports: [],
|
|
277
|
+
providers: rootProviders,
|
|
278
|
+
controllers: standaloneControllers,
|
|
279
|
+
commands: standaloneCommands,
|
|
280
|
+
queries: [],
|
|
281
|
+
exports: rootProviders.map((p) => p.token)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
74
285
|
const providedTokens = new Set(modules.flatMap((m) => m.providers.map((p) => p.token)));
|
|
75
286
|
const referenced = new Set;
|
|
76
287
|
for (const m of modules) {
|
|
@@ -89,7 +300,8 @@ async function analyzeProject(rootDir, include) {
|
|
|
89
300
|
modules,
|
|
90
301
|
externalTokens,
|
|
91
302
|
diagnostics: ctx.diagnostics,
|
|
92
|
-
tokenNames
|
|
303
|
+
tokenNames,
|
|
304
|
+
cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
|
|
93
305
|
};
|
|
94
306
|
}
|
|
95
307
|
function createProject(rootDir) {
|
|
@@ -124,7 +336,7 @@ function parseTokenVariable(decl, file) {
|
|
|
124
336
|
if (init.getExpression().getText() !== "InjectionToken")
|
|
125
337
|
return;
|
|
126
338
|
const [nameArg, optionsArg] = init.getArguments();
|
|
127
|
-
const info = { name: decl.getName(), file };
|
|
339
|
+
const info = { name: decl.getName(), file, line: decl.getStartLineNumber() };
|
|
128
340
|
if (nameArg && Node.isStringLiteral(nameArg)) {
|
|
129
341
|
info.stringName = nameArg.getLiteralText();
|
|
130
342
|
}
|
|
@@ -133,6 +345,14 @@ function parseTokenVariable(decl, file) {
|
|
|
133
345
|
if (scope && SCOPES.includes(scope)) {
|
|
134
346
|
info.scope = scope;
|
|
135
347
|
}
|
|
348
|
+
const providedIn = stringLiteralProp(optionsArg, "providedIn");
|
|
349
|
+
if (providedIn === "root") {
|
|
350
|
+
info.providedIn = "root";
|
|
351
|
+
}
|
|
352
|
+
const factory = getProp(optionsArg, "factory");
|
|
353
|
+
if (factory) {
|
|
354
|
+
info.hasFactory = true;
|
|
355
|
+
}
|
|
136
356
|
}
|
|
137
357
|
return info;
|
|
138
358
|
}
|
|
@@ -141,7 +361,8 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
141
361
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
142
362
|
const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
|
|
143
363
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
144
|
-
const
|
|
364
|
+
const unwrapped = unwrapForwardRef(el);
|
|
365
|
+
const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
|
|
145
366
|
if (decl) {
|
|
146
367
|
const known = nameByNode.get(decl);
|
|
147
368
|
if (known)
|
|
@@ -206,7 +427,8 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
206
427
|
permission: stringLiteralProp(meta, "permission"),
|
|
207
428
|
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
208
429
|
audit: stringLiteralProp(meta, "audit"),
|
|
209
|
-
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
430
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
431
|
+
standalone: booleanProp(meta, "standalone") || undefined
|
|
210
432
|
});
|
|
211
433
|
}
|
|
212
434
|
}
|
|
@@ -242,11 +464,13 @@ function commandModeProp(object, name) {
|
|
|
242
464
|
function parseProvider(el, exportsSet, ctx) {
|
|
243
465
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
244
466
|
const line = el.getStartLineNumber();
|
|
245
|
-
|
|
246
|
-
|
|
467
|
+
const unwrappedEl = unwrapForwardRef(el);
|
|
468
|
+
if (Node.isIdentifier(unwrappedEl)) {
|
|
469
|
+
const decl = resolveDeclaration(unwrappedEl)[0];
|
|
247
470
|
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
248
|
-
const className = cls?.getName() ??
|
|
249
|
-
const { deps, missing } = cls ? classDeps(cls, ctx) : { deps: [], missing: false };
|
|
471
|
+
const className = cls?.getName() ?? unwrappedEl.getText();
|
|
472
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], missing: false };
|
|
473
|
+
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
250
474
|
if (missing) {
|
|
251
475
|
warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
|
|
252
476
|
}
|
|
@@ -257,6 +481,12 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
257
481
|
useClass: className,
|
|
258
482
|
scope: resolveScope({ cls, tokenName: className }, ctx),
|
|
259
483
|
deps,
|
|
484
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
485
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
486
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
487
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
488
|
+
providedIn: injectable?.providedIn,
|
|
489
|
+
hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
|
|
260
490
|
exported: exportsSet.has(className),
|
|
261
491
|
file,
|
|
262
492
|
line,
|
|
@@ -271,22 +501,33 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
271
501
|
const { name: token, kind: tokenKind } = tokenNameOf(provideExpr, ctx);
|
|
272
502
|
const explicitScope = parseScopeProp(el);
|
|
273
503
|
const explicitDeps = arrayProp(el, "deps").map((d) => tokenNameOf(d, ctx).name);
|
|
504
|
+
const multi = booleanProp(el, "multi");
|
|
274
505
|
const useClassExpr = getProp(el, "useClass");
|
|
275
506
|
const useValueExpr = getProp(el, "useValue");
|
|
276
507
|
const useFactoryExpr = getProp(el, "useFactory");
|
|
277
508
|
const useExistingExpr = getProp(el, "useExisting");
|
|
278
509
|
if (useClassExpr) {
|
|
279
|
-
const
|
|
510
|
+
const unwrappedClass = unwrapForwardRef(useClassExpr);
|
|
511
|
+
const decl = Node.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass)[0] : undefined;
|
|
280
512
|
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
281
|
-
const useClass = cls?.getName() ??
|
|
513
|
+
const useClass = cls?.getName() ?? unwrappedClass.getText();
|
|
282
514
|
let deps = explicitDeps;
|
|
515
|
+
let optionalDeps = [];
|
|
516
|
+
let selfDeps = [];
|
|
517
|
+
let skipSelfDeps = [];
|
|
518
|
+
let hostDeps = [];
|
|
283
519
|
if (deps.length === 0 && cls) {
|
|
284
520
|
const result = classDeps(cls, ctx);
|
|
285
521
|
deps = result.deps;
|
|
522
|
+
optionalDeps = result.optionalDeps;
|
|
523
|
+
selfDeps = result.selfDeps;
|
|
524
|
+
skipSelfDeps = result.skipSelfDeps;
|
|
525
|
+
hostDeps = result.hostDeps;
|
|
286
526
|
if (result.missing) {
|
|
287
527
|
warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
|
|
288
528
|
}
|
|
289
529
|
}
|
|
530
|
+
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
290
531
|
return {
|
|
291
532
|
token,
|
|
292
533
|
tokenKind,
|
|
@@ -294,6 +535,13 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
294
535
|
useClass,
|
|
295
536
|
scope: resolveScope({ explicit: explicitScope, cls, tokenName: token }, ctx),
|
|
296
537
|
deps,
|
|
538
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
539
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
540
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
541
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
542
|
+
multi: multi ?? undefined,
|
|
543
|
+
providedIn: injectable?.providedIn,
|
|
544
|
+
hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
|
|
297
545
|
exported: exportsSet.has(token),
|
|
298
546
|
file,
|
|
299
547
|
line,
|
|
@@ -308,6 +556,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
308
556
|
useValueExpr: useValueExpr.getText(),
|
|
309
557
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
310
558
|
deps: [],
|
|
559
|
+
multi: multi ?? undefined,
|
|
311
560
|
exported: exportsSet.has(token),
|
|
312
561
|
file,
|
|
313
562
|
line,
|
|
@@ -326,6 +575,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
326
575
|
useFactoryName: factoryName,
|
|
327
576
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
328
577
|
deps: explicitDeps,
|
|
578
|
+
multi: multi ?? undefined,
|
|
329
579
|
exported: exportsSet.has(token),
|
|
330
580
|
file,
|
|
331
581
|
line,
|
|
@@ -341,6 +591,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
341
591
|
useExisting: target,
|
|
342
592
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
343
593
|
deps: [target],
|
|
594
|
+
multi: multi ?? undefined,
|
|
344
595
|
exported: exportsSet.has(token),
|
|
345
596
|
file,
|
|
346
597
|
line
|
|
@@ -348,18 +599,36 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
348
599
|
}
|
|
349
600
|
return;
|
|
350
601
|
}
|
|
351
|
-
function parseController(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
602
|
+
function parseController(input, ctx) {
|
|
603
|
+
let decl;
|
|
604
|
+
if (Node.isClassDeclaration(input)) {
|
|
605
|
+
decl = input;
|
|
606
|
+
} else {
|
|
607
|
+
const unwrapped = unwrapForwardRef(input);
|
|
608
|
+
const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
|
|
609
|
+
if (resolved && Node.isClassDeclaration(resolved)) {
|
|
610
|
+
decl = resolved;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (!decl)
|
|
356
614
|
return;
|
|
357
615
|
const controllerDec = findDecorator(decl, "Controller");
|
|
358
616
|
if (!controllerDec)
|
|
359
617
|
return;
|
|
618
|
+
let path = "/";
|
|
619
|
+
let standalone;
|
|
360
620
|
const pathArg = controllerDec.getArguments()[0];
|
|
361
|
-
|
|
362
|
-
|
|
621
|
+
if (pathArg) {
|
|
622
|
+
if (Node.isStringLiteral(pathArg)) {
|
|
623
|
+
path = pathArg.getLiteralText();
|
|
624
|
+
} else if (Node.isObjectLiteralExpression(pathArg)) {
|
|
625
|
+
const p = stringLiteralProp(pathArg, "path");
|
|
626
|
+
if (p)
|
|
627
|
+
path = p;
|
|
628
|
+
standalone = booleanProp(pathArg, "standalone");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
|
|
363
632
|
const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
|
|
364
633
|
if (missing) {
|
|
365
634
|
warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
|
|
@@ -367,6 +636,14 @@ function parseController(el, ctx) {
|
|
|
367
636
|
const injectable = parseInjectableOptions(decl, ctx);
|
|
368
637
|
const routes = [];
|
|
369
638
|
const schemaImports = {};
|
|
639
|
+
const classGuards = [];
|
|
640
|
+
for (const dec of decl.getDecorators()) {
|
|
641
|
+
if (decoratorName(dec) === "UseGuards") {
|
|
642
|
+
for (const gArg of dec.getArguments()) {
|
|
643
|
+
classGuards.push(tokenText(gArg));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
370
647
|
for (const method of decl.getMethods()) {
|
|
371
648
|
for (const dec of method.getDecorators()) {
|
|
372
649
|
const name = decoratorName(dec);
|
|
@@ -375,11 +652,158 @@ function parseController(el, ctx) {
|
|
|
375
652
|
continue;
|
|
376
653
|
const args = dec.getArguments();
|
|
377
654
|
const pathArg2 = args[0];
|
|
655
|
+
const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
|
|
378
656
|
const route = {
|
|
379
657
|
method: httpMethod,
|
|
380
|
-
path:
|
|
658
|
+
path: routePath,
|
|
381
659
|
handler: method.getName()
|
|
382
660
|
};
|
|
661
|
+
const pathParams = [];
|
|
662
|
+
const paramRegex = /:([a-zA-Z0-9_]+)/g;
|
|
663
|
+
let match;
|
|
664
|
+
while ((match = paramRegex.exec(routePath)) !== null) {
|
|
665
|
+
pathParams.push(match[1]);
|
|
666
|
+
}
|
|
667
|
+
if (pathParams.length > 0)
|
|
668
|
+
route.pathParams = pathParams;
|
|
669
|
+
const paramBindings = [];
|
|
670
|
+
const queryBindings = [];
|
|
671
|
+
const paramTransforms = {};
|
|
672
|
+
const paramDefaults = {};
|
|
673
|
+
const queryTransforms = {};
|
|
674
|
+
const queryDefaults = {};
|
|
675
|
+
let hasBodyBinding = false;
|
|
676
|
+
const handlerParams = [];
|
|
677
|
+
for (const p of method.getParameters()) {
|
|
678
|
+
const pName = p.getName();
|
|
679
|
+
let hasBindingDecorator = false;
|
|
680
|
+
let paramNode;
|
|
681
|
+
for (const pDec of p.getDecorators()) {
|
|
682
|
+
const dName = decoratorName(pDec);
|
|
683
|
+
const dArgs = pDec.getArguments();
|
|
684
|
+
if (dName === "Param") {
|
|
685
|
+
hasBindingDecorator = true;
|
|
686
|
+
const parsed = parseBindingOptions(dArgs, pName);
|
|
687
|
+
paramBindings.push(parsed.name);
|
|
688
|
+
if (parsed.transform)
|
|
689
|
+
paramTransforms[parsed.name] = parsed.transform;
|
|
690
|
+
if (parsed.default !== undefined)
|
|
691
|
+
paramDefaults[parsed.name] = parsed.default;
|
|
692
|
+
paramNode = {
|
|
693
|
+
name: pName,
|
|
694
|
+
kind: "param",
|
|
695
|
+
bindingName: parsed.name,
|
|
696
|
+
transform: parsed.transform,
|
|
697
|
+
default: parsed.default
|
|
698
|
+
};
|
|
699
|
+
} else if (dName === "Query") {
|
|
700
|
+
hasBindingDecorator = true;
|
|
701
|
+
const parsed = parseBindingOptions(dArgs, pName);
|
|
702
|
+
queryBindings.push(parsed.name);
|
|
703
|
+
if (parsed.transform)
|
|
704
|
+
queryTransforms[parsed.name] = parsed.transform;
|
|
705
|
+
if (parsed.default !== undefined)
|
|
706
|
+
queryDefaults[parsed.name] = parsed.default;
|
|
707
|
+
paramNode = {
|
|
708
|
+
name: pName,
|
|
709
|
+
kind: "query",
|
|
710
|
+
bindingName: parsed.name,
|
|
711
|
+
transform: parsed.transform,
|
|
712
|
+
default: parsed.default
|
|
713
|
+
};
|
|
714
|
+
} else if (dName === "Body") {
|
|
715
|
+
hasBindingDecorator = true;
|
|
716
|
+
hasBodyBinding = true;
|
|
717
|
+
paramNode = { name: pName, kind: "body" };
|
|
718
|
+
} else if (dName === "Headers") {
|
|
719
|
+
hasBindingDecorator = true;
|
|
720
|
+
paramNode = { name: pName, kind: "headers" };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!hasBindingDecorator && pathParams.includes(pName)) {
|
|
724
|
+
paramBindings.push(pName);
|
|
725
|
+
const typeText = p.getType().getText();
|
|
726
|
+
let inferredTransform;
|
|
727
|
+
if (typeText === "number") {
|
|
728
|
+
paramTransforms[pName] = "number";
|
|
729
|
+
inferredTransform = "number";
|
|
730
|
+
} else if (typeText === "boolean") {
|
|
731
|
+
paramTransforms[pName] = "boolean";
|
|
732
|
+
inferredTransform = "boolean";
|
|
733
|
+
}
|
|
734
|
+
paramNode = {
|
|
735
|
+
name: pName,
|
|
736
|
+
kind: "param",
|
|
737
|
+
bindingName: pName,
|
|
738
|
+
transform: inferredTransform
|
|
739
|
+
};
|
|
740
|
+
} else if (!hasBindingDecorator) {
|
|
741
|
+
if (pName === "req" || pName === "ctx" || pName === "context") {
|
|
742
|
+
paramNode = { name: pName, kind: "context" };
|
|
743
|
+
} else {
|
|
744
|
+
paramNode = { name: pName, kind: "unknown" };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (paramNode)
|
|
748
|
+
handlerParams.push(paramNode);
|
|
749
|
+
}
|
|
750
|
+
if (paramBindings.length > 0)
|
|
751
|
+
route.paramBindings = paramBindings;
|
|
752
|
+
if (queryBindings.length > 0)
|
|
753
|
+
route.queryBindings = queryBindings;
|
|
754
|
+
if (Object.keys(paramTransforms).length > 0)
|
|
755
|
+
route.paramTransforms = paramTransforms;
|
|
756
|
+
if (Object.keys(paramDefaults).length > 0)
|
|
757
|
+
route.paramDefaults = paramDefaults;
|
|
758
|
+
if (Object.keys(queryTransforms).length > 0)
|
|
759
|
+
route.queryTransforms = queryTransforms;
|
|
760
|
+
if (Object.keys(queryDefaults).length > 0)
|
|
761
|
+
route.queryDefaults = queryDefaults;
|
|
762
|
+
if (hasBodyBinding)
|
|
763
|
+
route.hasBodyBinding = true;
|
|
764
|
+
if (handlerParams.length > 0)
|
|
765
|
+
route.handlerParams = handlerParams;
|
|
766
|
+
const routeGuards = [...classGuards];
|
|
767
|
+
const routeCanDeactivate = [];
|
|
768
|
+
for (const mDec of method.getDecorators()) {
|
|
769
|
+
const dName = decoratorName(mDec);
|
|
770
|
+
const mArgs = mDec.getArguments();
|
|
771
|
+
if (dName === "UseGuards") {
|
|
772
|
+
for (const gArg of mArgs) {
|
|
773
|
+
routeGuards.push(tokenText(gArg));
|
|
774
|
+
}
|
|
775
|
+
} else if (dName === "CanDeactivate") {
|
|
776
|
+
for (const gArg of mArgs) {
|
|
777
|
+
routeCanDeactivate.push(tokenText(gArg));
|
|
778
|
+
}
|
|
779
|
+
} else if (dName === "Title") {
|
|
780
|
+
const tArg = mArgs[0];
|
|
781
|
+
if (tArg && Node.isStringLiteral(tArg)) {
|
|
782
|
+
route.title = tArg.getLiteralText();
|
|
783
|
+
}
|
|
784
|
+
} else if (dName === "Data") {
|
|
785
|
+
const dArg = mArgs[0];
|
|
786
|
+
if (dArg && Node.isObjectLiteralExpression(dArg)) {
|
|
787
|
+
route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
|
|
788
|
+
}
|
|
789
|
+
} else if (dName === "Resolve") {
|
|
790
|
+
const rArg = mArgs[0];
|
|
791
|
+
if (rArg && Node.isObjectLiteralExpression(rArg)) {
|
|
792
|
+
const resolvers = route.resolvers ?? {};
|
|
793
|
+
for (const prop of rArg.getProperties()) {
|
|
794
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
795
|
+
const rName = prop.getName();
|
|
796
|
+
const init = prop.getInitializer();
|
|
797
|
+
if (init)
|
|
798
|
+
resolvers[rName] = tokenText(init);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (Object.keys(resolvers).length > 0) {
|
|
802
|
+
route.resolvers = resolvers;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
383
807
|
const optionsArg = args[1];
|
|
384
808
|
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
385
809
|
for (const field of ["body", "params", "query", "response"]) {
|
|
@@ -396,6 +820,68 @@ function parseController(el, ctx) {
|
|
|
396
820
|
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
397
821
|
route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
398
822
|
}
|
|
823
|
+
const guardsExpr = getProp(optionsArg, "guards");
|
|
824
|
+
if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
|
|
825
|
+
for (const el of guardsExpr.getElements()) {
|
|
826
|
+
routeGuards.push(tokenText(el));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const canMatchExpr = getProp(optionsArg, "canMatch");
|
|
830
|
+
if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
|
|
831
|
+
const canMatchList = [];
|
|
832
|
+
for (const el of canMatchExpr.getElements()) {
|
|
833
|
+
canMatchList.push(tokenText(el));
|
|
834
|
+
}
|
|
835
|
+
if (canMatchList.length > 0) {
|
|
836
|
+
route.canMatch = canMatchList;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
|
|
840
|
+
if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
|
|
841
|
+
for (const el of canDeactivateExpr.getElements()) {
|
|
842
|
+
routeCanDeactivate.push(tokenText(el));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const resolversExpr = getProp(optionsArg, "resolvers");
|
|
846
|
+
if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
|
|
847
|
+
const resolvers = {};
|
|
848
|
+
for (const prop of resolversExpr.getProperties()) {
|
|
849
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
850
|
+
const rName = prop.getName();
|
|
851
|
+
const init = prop.getInitializer();
|
|
852
|
+
if (init)
|
|
853
|
+
resolvers[rName] = tokenText(init);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (Object.keys(resolvers).length > 0) {
|
|
857
|
+
route.resolvers = resolvers;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const redirectToExpr = getProp(optionsArg, "redirectTo");
|
|
861
|
+
if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
|
|
862
|
+
route.redirectTo = redirectToExpr.getLiteralText();
|
|
863
|
+
}
|
|
864
|
+
const pathMatchExpr = getProp(optionsArg, "pathMatch");
|
|
865
|
+
if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
|
|
866
|
+
const val = pathMatchExpr.getLiteralText();
|
|
867
|
+
if (val === "full" || val === "prefix") {
|
|
868
|
+
route.pathMatch = val;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const titleExpr = getProp(optionsArg, "title");
|
|
872
|
+
if (titleExpr && Node.isStringLiteral(titleExpr)) {
|
|
873
|
+
route.title = titleExpr.getLiteralText();
|
|
874
|
+
}
|
|
875
|
+
const dataExpr = getProp(optionsArg, "data");
|
|
876
|
+
if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
|
|
877
|
+
route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (routeGuards.length > 0) {
|
|
881
|
+
route.guards = routeGuards;
|
|
882
|
+
}
|
|
883
|
+
if (routeCanDeactivate.length > 0) {
|
|
884
|
+
route.canDeactivate = routeCanDeactivate;
|
|
399
885
|
}
|
|
400
886
|
routes.push(route);
|
|
401
887
|
}
|
|
@@ -405,6 +891,10 @@ function parseController(el, ctx) {
|
|
|
405
891
|
path,
|
|
406
892
|
scope: injectable?.scope ?? "request",
|
|
407
893
|
deps,
|
|
894
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
895
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
896
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
897
|
+
standalone: standalone || undefined,
|
|
408
898
|
routes,
|
|
409
899
|
file,
|
|
410
900
|
importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
|
|
@@ -413,28 +903,84 @@ function parseController(el, ctx) {
|
|
|
413
903
|
}
|
|
414
904
|
function classDeps(cls, ctx) {
|
|
415
905
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
416
|
-
if (injectable?.deps)
|
|
417
|
-
return {
|
|
906
|
+
if (injectable?.deps) {
|
|
907
|
+
return {
|
|
908
|
+
deps: injectable.deps,
|
|
909
|
+
optionalDeps: [],
|
|
910
|
+
selfDeps: [],
|
|
911
|
+
skipSelfDeps: [],
|
|
912
|
+
hostDeps: [],
|
|
913
|
+
missing: false
|
|
914
|
+
};
|
|
915
|
+
}
|
|
418
916
|
const ctor = cls.getConstructors()[0];
|
|
419
|
-
if (!ctor || ctor.getParameters().length === 0)
|
|
420
|
-
return { deps: [], missing: false };
|
|
421
|
-
const injectParams = parseInjectParams(cls);
|
|
422
917
|
const deps = [];
|
|
918
|
+
const optionalDeps = [];
|
|
919
|
+
const selfDeps = [];
|
|
920
|
+
const skipSelfDeps = [];
|
|
921
|
+
const hostDeps = [];
|
|
423
922
|
let missing = false;
|
|
424
|
-
ctor.getParameters().
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
923
|
+
if (ctor && ctor.getParameters().length > 0) {
|
|
924
|
+
const injectParams = parseInjectParams(cls);
|
|
925
|
+
const optionalIndices = parseOptionalParams(cls);
|
|
926
|
+
const selfIndices = parseModifierParams(cls, "Self");
|
|
927
|
+
const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
|
|
928
|
+
const hostIndices = parseModifierParams(cls, "Host");
|
|
929
|
+
ctor.getParameters().forEach((param, index) => {
|
|
930
|
+
const isOptional = optionalIndices.has(index);
|
|
931
|
+
const injected = injectParams.get(index);
|
|
932
|
+
const tokenName = injected ?? paramTypeTokenName(param, ctx);
|
|
933
|
+
if (tokenName) {
|
|
934
|
+
deps.push(tokenName);
|
|
935
|
+
if (isOptional)
|
|
936
|
+
optionalDeps.push(tokenName);
|
|
937
|
+
if (selfIndices.has(index))
|
|
938
|
+
selfDeps.push(tokenName);
|
|
939
|
+
if (skipSelfIndices.has(index))
|
|
940
|
+
skipSelfDeps.push(tokenName);
|
|
941
|
+
if (hostIndices.has(index))
|
|
942
|
+
hostDeps.push(tokenName);
|
|
943
|
+
} else {
|
|
944
|
+
if (!isOptional)
|
|
945
|
+
missing = true;
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
for (const prop of cls.getProperties()) {
|
|
950
|
+
const init = prop.getInitializer();
|
|
951
|
+
if (init && Node.isCallExpression(init)) {
|
|
952
|
+
const callName = init.getExpression().getText().split(".").pop();
|
|
953
|
+
if (callName === "inject") {
|
|
954
|
+
const [tokenArg, optionsArg] = init.getArguments();
|
|
955
|
+
if (tokenArg) {
|
|
956
|
+
const tokenName = tokenText(tokenArg);
|
|
957
|
+
if (tokenName) {
|
|
958
|
+
if (!deps.includes(tokenName))
|
|
959
|
+
deps.push(tokenName);
|
|
960
|
+
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
961
|
+
const isOptional = booleanProp(optionsArg, "optional");
|
|
962
|
+
if (isOptional && !optionalDeps.includes(tokenName)) {
|
|
963
|
+
optionalDeps.push(tokenName);
|
|
964
|
+
}
|
|
965
|
+
const isSelf = booleanProp(optionsArg, "self");
|
|
966
|
+
if (isSelf && !selfDeps.includes(tokenName)) {
|
|
967
|
+
selfDeps.push(tokenName);
|
|
968
|
+
}
|
|
969
|
+
const isSkipSelf = booleanProp(optionsArg, "skipSelf");
|
|
970
|
+
if (isSkipSelf && !skipSelfDeps.includes(tokenName)) {
|
|
971
|
+
skipSelfDeps.push(tokenName);
|
|
972
|
+
}
|
|
973
|
+
const isHost = booleanProp(optionsArg, "host");
|
|
974
|
+
if (isHost && !hostDeps.includes(tokenName)) {
|
|
975
|
+
hostDeps.push(tokenName);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
435
981
|
}
|
|
436
|
-
}
|
|
437
|
-
return { deps, missing };
|
|
982
|
+
}
|
|
983
|
+
return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
|
|
438
984
|
}
|
|
439
985
|
function paramTypeTokenName(param, ctx) {
|
|
440
986
|
const typeNode = param.getTypeNode();
|
|
@@ -455,9 +1001,11 @@ function parseInjectableOptions(cls, ctx) {
|
|
|
455
1001
|
if (!obj)
|
|
456
1002
|
return {};
|
|
457
1003
|
const scope = stringLiteralProp(obj, "scope");
|
|
1004
|
+
const providedIn = stringLiteralProp(obj, "providedIn");
|
|
458
1005
|
const depsExpr = getProp(obj, "deps");
|
|
459
1006
|
return {
|
|
460
1007
|
scope: scope && SCOPES.includes(scope) ? scope : undefined,
|
|
1008
|
+
providedIn: providedIn === "root" ? "root" : undefined,
|
|
461
1009
|
deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
|
|
462
1010
|
};
|
|
463
1011
|
}
|
|
@@ -477,15 +1025,61 @@ function parseInjectParams(cls) {
|
|
|
477
1025
|
});
|
|
478
1026
|
return result;
|
|
479
1027
|
}
|
|
1028
|
+
function parseOptionalParams(cls) {
|
|
1029
|
+
const result = new Set;
|
|
1030
|
+
const ctor = cls.getConstructors()[0];
|
|
1031
|
+
if (!ctor)
|
|
1032
|
+
return result;
|
|
1033
|
+
ctor.getParameters().forEach((param, index) => {
|
|
1034
|
+
for (const dec of param.getDecorators()) {
|
|
1035
|
+
if (decoratorName(dec) === "Optional")
|
|
1036
|
+
result.add(index);
|
|
1037
|
+
}
|
|
1038
|
+
if (param.hasQuestionToken())
|
|
1039
|
+
result.add(index);
|
|
1040
|
+
});
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
function parseModifierParams(cls, modifierName) {
|
|
1044
|
+
const result = new Set;
|
|
1045
|
+
const ctor = cls.getConstructors()[0];
|
|
1046
|
+
if (!ctor)
|
|
1047
|
+
return result;
|
|
1048
|
+
ctor.getParameters().forEach((param, index) => {
|
|
1049
|
+
for (const dec of param.getDecorators()) {
|
|
1050
|
+
if (decoratorName(dec) === modifierName)
|
|
1051
|
+
result.add(index);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
function unwrapForwardRef(expr) {
|
|
1057
|
+
if (Node.isCallExpression(expr)) {
|
|
1058
|
+
const exprText = expr.getExpression().getText();
|
|
1059
|
+
if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
|
|
1060
|
+
const arg = expr.getArguments()[0];
|
|
1061
|
+
if (arg && (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg))) {
|
|
1062
|
+
const body = arg.getBody();
|
|
1063
|
+
if (body && Node.isExpression(body)) {
|
|
1064
|
+
return unwrapForwardRef(body);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return expr;
|
|
1070
|
+
}
|
|
480
1071
|
function tokenText(expr) {
|
|
481
|
-
|
|
482
|
-
|
|
1072
|
+
const unwrapped = unwrapForwardRef(expr);
|
|
1073
|
+
if (Node.isStringLiteral(unwrapped))
|
|
1074
|
+
return unwrapped.getLiteralText();
|
|
1075
|
+
if (Node.isIdentifier(unwrapped)) {
|
|
1076
|
+
const decl = resolveDeclaration(unwrapped)[0];
|
|
483
1077
|
if (decl && Node.isClassDeclaration(decl))
|
|
484
|
-
return decl.getName() ??
|
|
1078
|
+
return decl.getName() ?? unwrapped.getText();
|
|
485
1079
|
if (decl && Node.isVariableDeclaration(decl))
|
|
486
1080
|
return decl.getName();
|
|
487
1081
|
}
|
|
488
|
-
return
|
|
1082
|
+
return unwrapped.getText();
|
|
489
1083
|
}
|
|
490
1084
|
function resolveScope(input, ctx) {
|
|
491
1085
|
if (input.explicit)
|
|
@@ -501,8 +1095,9 @@ function resolveScope(input, ctx) {
|
|
|
501
1095
|
return "application";
|
|
502
1096
|
}
|
|
503
1097
|
function tokenNameOf(expr, ctx) {
|
|
504
|
-
|
|
505
|
-
|
|
1098
|
+
const unwrapped = unwrapForwardRef(expr);
|
|
1099
|
+
if (Node.isIdentifier(unwrapped)) {
|
|
1100
|
+
const decl = resolveDeclaration(unwrapped)[0];
|
|
506
1101
|
if (decl && Node.isClassDeclaration(decl)) {
|
|
507
1102
|
return { name: decl.getName() ?? expr.getText(), kind: "class" };
|
|
508
1103
|
}
|
|
@@ -580,10 +1175,90 @@ function arrayProp(obj, name) {
|
|
|
580
1175
|
const expr = getProp(obj, name);
|
|
581
1176
|
return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
|
|
582
1177
|
}
|
|
1178
|
+
function booleanProp(obj, name) {
|
|
1179
|
+
const expr = getProp(obj, name);
|
|
1180
|
+
if (!expr)
|
|
1181
|
+
return;
|
|
1182
|
+
if (expr.getKind() === SyntaxKind.TrueKeyword)
|
|
1183
|
+
return true;
|
|
1184
|
+
if (expr.getKind() === SyntaxKind.FalseKeyword)
|
|
1185
|
+
return false;
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
583
1188
|
function parseScopeProp(obj) {
|
|
584
1189
|
const scope = stringLiteralProp(obj, "scope");
|
|
585
1190
|
return scope && SCOPES.includes(scope) ? scope : undefined;
|
|
586
1191
|
}
|
|
1192
|
+
function parseBindingOptions(args, defaultName) {
|
|
1193
|
+
let name = defaultName;
|
|
1194
|
+
let transform;
|
|
1195
|
+
let defaultValue;
|
|
1196
|
+
const first = args[0];
|
|
1197
|
+
const second = args[1];
|
|
1198
|
+
if (first && Node.isStringLiteral(first)) {
|
|
1199
|
+
name = first.getLiteralText();
|
|
1200
|
+
} else if (first && Node.isObjectLiteralExpression(first)) {
|
|
1201
|
+
const nameProp = getProp(first, "name");
|
|
1202
|
+
if (nameProp && Node.isStringLiteral(nameProp)) {
|
|
1203
|
+
name = nameProp.getLiteralText();
|
|
1204
|
+
}
|
|
1205
|
+
const trProp = getProp(first, "transform");
|
|
1206
|
+
if (trProp && Node.isStringLiteral(trProp)) {
|
|
1207
|
+
const val = trProp.getLiteralText();
|
|
1208
|
+
if (val === "number" || val === "boolean" || val === "string") {
|
|
1209
|
+
transform = val;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const defProp = getProp(first, "default");
|
|
1213
|
+
if (defProp) {
|
|
1214
|
+
defaultValue = parseLiteralValue(defProp);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (second && Node.isObjectLiteralExpression(second)) {
|
|
1218
|
+
const trProp = getProp(second, "transform");
|
|
1219
|
+
if (trProp && Node.isStringLiteral(trProp)) {
|
|
1220
|
+
const val = trProp.getLiteralText();
|
|
1221
|
+
if (val === "number" || val === "boolean" || val === "string") {
|
|
1222
|
+
transform = val;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const defProp = getProp(second, "default");
|
|
1226
|
+
if (defProp) {
|
|
1227
|
+
defaultValue = parseLiteralValue(defProp);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return { name, transform, default: defaultValue };
|
|
1231
|
+
}
|
|
1232
|
+
function parseLiteralValue(node) {
|
|
1233
|
+
if (Node.isStringLiteral(node))
|
|
1234
|
+
return node.getLiteralText();
|
|
1235
|
+
if (Node.isNumericLiteral(node))
|
|
1236
|
+
return node.getLiteralValue();
|
|
1237
|
+
if (node.getKindName() === "TrueKeyword")
|
|
1238
|
+
return true;
|
|
1239
|
+
if (node.getKindName() === "FalseKeyword")
|
|
1240
|
+
return false;
|
|
1241
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
1242
|
+
return node.getElements().map(parseLiteralValue);
|
|
1243
|
+
}
|
|
1244
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
1245
|
+
return parseObjectLiteralValues(node);
|
|
1246
|
+
}
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
function parseObjectLiteralValues(obj) {
|
|
1250
|
+
const result = {};
|
|
1251
|
+
for (const prop of obj.getProperties()) {
|
|
1252
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
1253
|
+
const name = prop.getName();
|
|
1254
|
+
const init = prop.getInitializer();
|
|
1255
|
+
if (init) {
|
|
1256
|
+
result[name] = parseLiteralValue(init);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
587
1262
|
function modulePath(rootDir, absFile) {
|
|
588
1263
|
return sourcePath(rootDir, absFile).replace(/\.(ts|tsx|js|mts|cts)$/, "");
|
|
589
1264
|
}
|
|
@@ -594,7 +1269,7 @@ function warn(ctx, code, message, file, line) {
|
|
|
594
1269
|
ctx.diagnostics.push({ severity: "warn", code, message, file, line });
|
|
595
1270
|
}
|
|
596
1271
|
// src/generate.ts
|
|
597
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1272
|
+
import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
598
1273
|
import { join as join2 } from "node:path";
|
|
599
1274
|
|
|
600
1275
|
// src/util.ts
|
|
@@ -627,6 +1302,28 @@ function isRequestContextToken(token, tokenNames) {
|
|
|
627
1302
|
function isJobContextToken(token, tokenNames) {
|
|
628
1303
|
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
629
1304
|
}
|
|
1305
|
+
function joinRoutePaths(prefix, path) {
|
|
1306
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1307
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1308
|
+
return normalized;
|
|
1309
|
+
}
|
|
1310
|
+
function findClosestMatch(target, candidates) {
|
|
1311
|
+
if (candidates.length === 0)
|
|
1312
|
+
return;
|
|
1313
|
+
if (candidates.length === 1)
|
|
1314
|
+
return candidates[0];
|
|
1315
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1316
|
+
const targetNorm = norm(target);
|
|
1317
|
+
for (const c of candidates) {
|
|
1318
|
+
if (norm(c) === targetNorm)
|
|
1319
|
+
return c;
|
|
1320
|
+
}
|
|
1321
|
+
for (const c of candidates) {
|
|
1322
|
+
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
1323
|
+
return c;
|
|
1324
|
+
}
|
|
1325
|
+
return candidates[0];
|
|
1326
|
+
}
|
|
630
1327
|
|
|
631
1328
|
// src/generate.ts
|
|
632
1329
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
@@ -639,6 +1336,28 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
639
1336
|
query?: unknown;
|
|
640
1337
|
response?: unknown;
|
|
641
1338
|
command?: string;
|
|
1339
|
+
guards?: string[];
|
|
1340
|
+
canMatch?: string[];
|
|
1341
|
+
canDeactivate?: string[];
|
|
1342
|
+
resolvers?: Record<string, string>;
|
|
1343
|
+
redirectTo?: string;
|
|
1344
|
+
pathMatch?: "full" | "prefix";
|
|
1345
|
+
paramTransforms?: Record<string, "number" | "boolean" | "string">;
|
|
1346
|
+
paramDefaults?: Record<string, unknown>;
|
|
1347
|
+
queryTransforms?: Record<string, "number" | "boolean" | "string">;
|
|
1348
|
+
queryDefaults?: Record<string, unknown>;
|
|
1349
|
+
title?: string;
|
|
1350
|
+
data?: Record<string, unknown>;
|
|
1351
|
+
invoker?: (
|
|
1352
|
+
controller: unknown,
|
|
1353
|
+
request: {
|
|
1354
|
+
params?: Record<string, unknown>;
|
|
1355
|
+
query?: Record<string, unknown>;
|
|
1356
|
+
body?: unknown;
|
|
1357
|
+
headers?: Record<string, unknown>;
|
|
1358
|
+
context?: unknown;
|
|
1359
|
+
},
|
|
1360
|
+
) => Promise<unknown> | unknown;
|
|
642
1361
|
}
|
|
643
1362
|
|
|
644
1363
|
export interface CompiledCommand {
|
|
@@ -648,6 +1367,7 @@ export interface CompiledCommand {
|
|
|
648
1367
|
transaction: "required" | "none";
|
|
649
1368
|
audit?: string;
|
|
650
1369
|
idempotency: "required" | "none";
|
|
1370
|
+
standalone?: boolean;
|
|
651
1371
|
}
|
|
652
1372
|
|
|
653
1373
|
export interface CompiledController {
|
|
@@ -677,7 +1397,40 @@ export interface CompiledModule {
|
|
|
677
1397
|
commands: CompiledCommand[];
|
|
678
1398
|
}`;
|
|
679
1399
|
function renderApplication(graph, options) {
|
|
680
|
-
|
|
1400
|
+
let modules = topoSortModules(graph.modules);
|
|
1401
|
+
if (options.treeShakeUnusedProviders) {
|
|
1402
|
+
const referencedTokens = new Set;
|
|
1403
|
+
for (const mod of graph.modules) {
|
|
1404
|
+
for (const exp of mod.exports)
|
|
1405
|
+
referencedTokens.add(exp);
|
|
1406
|
+
for (const ctrl of mod.controllers) {
|
|
1407
|
+
for (const d of ctrl.deps)
|
|
1408
|
+
referencedTokens.add(d);
|
|
1409
|
+
for (const d of ctrl.optionalDeps ?? [])
|
|
1410
|
+
referencedTokens.add(d);
|
|
1411
|
+
for (const d of ctrl.selfDeps ?? [])
|
|
1412
|
+
referencedTokens.add(d);
|
|
1413
|
+
for (const d of ctrl.skipSelfDeps ?? [])
|
|
1414
|
+
referencedTokens.add(d);
|
|
1415
|
+
}
|
|
1416
|
+
for (const p of mod.providers) {
|
|
1417
|
+
for (const d of p.deps ?? [])
|
|
1418
|
+
referencedTokens.add(d);
|
|
1419
|
+
for (const d of p.optionalDeps ?? [])
|
|
1420
|
+
referencedTokens.add(d);
|
|
1421
|
+
for (const d of p.selfDeps ?? [])
|
|
1422
|
+
referencedTokens.add(d);
|
|
1423
|
+
for (const d of p.skipSelfDeps ?? [])
|
|
1424
|
+
referencedTokens.add(d);
|
|
1425
|
+
if (p.useExisting)
|
|
1426
|
+
referencedTokens.add(p.useExisting);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
modules = modules.map((mod) => ({
|
|
1430
|
+
...mod,
|
|
1431
|
+
providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
|
|
1432
|
+
}));
|
|
1433
|
+
}
|
|
681
1434
|
const imports = new ImportManager;
|
|
682
1435
|
const factorySections = [];
|
|
683
1436
|
const descriptorEntries = [];
|
|
@@ -699,6 +1452,34 @@ function renderApplication(graph, options) {
|
|
|
699
1452
|
" ];",
|
|
700
1453
|
"}",
|
|
701
1454
|
"",
|
|
1455
|
+
"export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1456
|
+
' const initializers = (services.appInitializer ?? (services as any)["supacloud.app-initializer"]) as unknown;',
|
|
1457
|
+
" if (Array.isArray(initializers)) {",
|
|
1458
|
+
" for (const init of initializers) {",
|
|
1459
|
+
' if (typeof init === "function") await init();',
|
|
1460
|
+
" }",
|
|
1461
|
+
' } else if (typeof initializers === "function") {',
|
|
1462
|
+
" await (initializers as () => unknown)();",
|
|
1463
|
+
" }",
|
|
1464
|
+
"}",
|
|
1465
|
+
"",
|
|
1466
|
+
"export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1467
|
+
' const destroyRef = (services.destroyRef ?? (services as any)["supacloud.destroy-ref"]) as { destroy?: () => Promise<void>; _teardowns?: Array<() => void | Promise<void>> } | undefined;',
|
|
1468
|
+
' if (destroyRef && typeof destroyRef.destroy === "function") {',
|
|
1469
|
+
" await destroyRef.destroy();",
|
|
1470
|
+
" } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
|
|
1471
|
+
" for (const teardown of [...destroyRef._teardowns].reverse()) {",
|
|
1472
|
+
' if (typeof teardown === "function") await teardown();',
|
|
1473
|
+
" }",
|
|
1474
|
+
" }",
|
|
1475
|
+
" const instances = Object.values(services);",
|
|
1476
|
+
" for (const inst of instances.reverse()) {",
|
|
1477
|
+
' if (inst && typeof (inst as any).onDestroy === "function") {',
|
|
1478
|
+
" await (inst as any).onDestroy();",
|
|
1479
|
+
" }",
|
|
1480
|
+
" }",
|
|
1481
|
+
"}",
|
|
1482
|
+
"",
|
|
702
1483
|
...factorySections,
|
|
703
1484
|
""
|
|
704
1485
|
].join(`
|
|
@@ -708,10 +1489,14 @@ function renderApplication(graph, options) {
|
|
|
708
1489
|
modules: graph.modules,
|
|
709
1490
|
externalTokens: graph.externalTokens
|
|
710
1491
|
};
|
|
1492
|
+
const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
|
|
1493
|
+
const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
|
|
711
1494
|
return {
|
|
712
1495
|
applicationCode: code,
|
|
713
1496
|
manifestJson: JSON.stringify(manifest, null, 2) + `
|
|
714
|
-
|
|
1497
|
+
`,
|
|
1498
|
+
clientCode,
|
|
1499
|
+
permissionsCode
|
|
715
1500
|
};
|
|
716
1501
|
}
|
|
717
1502
|
async function generateApplication(graph, options) {
|
|
@@ -719,9 +1504,32 @@ async function generateApplication(graph, options) {
|
|
|
719
1504
|
await mkdir(options.outDir, { recursive: true });
|
|
720
1505
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
721
1506
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
722
|
-
await
|
|
723
|
-
await
|
|
724
|
-
|
|
1507
|
+
await writeFileAtomic(applicationPath, rendered.applicationCode);
|
|
1508
|
+
await writeFileAtomic(manifestPath, rendered.manifestJson);
|
|
1509
|
+
const written = [applicationPath, manifestPath];
|
|
1510
|
+
if (rendered.clientCode) {
|
|
1511
|
+
const clientPath = join2(options.outDir, "client.ts");
|
|
1512
|
+
await writeFileAtomic(clientPath, rendered.clientCode);
|
|
1513
|
+
written.push(clientPath);
|
|
1514
|
+
}
|
|
1515
|
+
if (rendered.permissionsCode) {
|
|
1516
|
+
const permissionsPath = join2(options.outDir, "permissions.ts");
|
|
1517
|
+
await writeFileAtomic(permissionsPath, rendered.permissionsCode);
|
|
1518
|
+
written.push(permissionsPath);
|
|
1519
|
+
}
|
|
1520
|
+
return written;
|
|
1521
|
+
}
|
|
1522
|
+
async function writeFileAtomic(path, content) {
|
|
1523
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1524
|
+
try {
|
|
1525
|
+
await writeFile(temporaryPath, content, "utf8");
|
|
1526
|
+
await rename(temporaryPath, path);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
await unlink(temporaryPath).catch(() => {
|
|
1529
|
+
return;
|
|
1530
|
+
});
|
|
1531
|
+
throw error;
|
|
1532
|
+
}
|
|
725
1533
|
}
|
|
726
1534
|
function factoryOfScope(scope) {
|
|
727
1535
|
return scope === "application" ? "services" : scope;
|
|
@@ -858,6 +1666,81 @@ class ModuleGenerator {
|
|
|
858
1666
|
}
|
|
859
1667
|
if (route.command)
|
|
860
1668
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1669
|
+
if (route.guards && route.guards.length > 0) {
|
|
1670
|
+
fields.push(`guards: ${JSON.stringify(route.guards)}`);
|
|
1671
|
+
}
|
|
1672
|
+
if (route.canMatch && route.canMatch.length > 0) {
|
|
1673
|
+
fields.push(`canMatch: ${JSON.stringify(route.canMatch)}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (route.canDeactivate && route.canDeactivate.length > 0) {
|
|
1676
|
+
fields.push(`canDeactivate: ${JSON.stringify(route.canDeactivate)}`);
|
|
1677
|
+
}
|
|
1678
|
+
if (route.resolvers && Object.keys(route.resolvers).length > 0) {
|
|
1679
|
+
fields.push(`resolvers: ${JSON.stringify(route.resolvers)}`);
|
|
1680
|
+
}
|
|
1681
|
+
if (route.redirectTo) {
|
|
1682
|
+
fields.push(`redirectTo: ${JSON.stringify(route.redirectTo)}`);
|
|
1683
|
+
}
|
|
1684
|
+
if (route.pathMatch) {
|
|
1685
|
+
fields.push(`pathMatch: ${JSON.stringify(route.pathMatch)}`);
|
|
1686
|
+
}
|
|
1687
|
+
if (route.paramTransforms && Object.keys(route.paramTransforms).length > 0) {
|
|
1688
|
+
fields.push(`paramTransforms: ${JSON.stringify(route.paramTransforms)}`);
|
|
1689
|
+
}
|
|
1690
|
+
if (route.paramDefaults && Object.keys(route.paramDefaults).length > 0) {
|
|
1691
|
+
fields.push(`paramDefaults: ${JSON.stringify(route.paramDefaults)}`);
|
|
1692
|
+
}
|
|
1693
|
+
if (route.queryTransforms && Object.keys(route.queryTransforms).length > 0) {
|
|
1694
|
+
fields.push(`queryTransforms: ${JSON.stringify(route.queryTransforms)}`);
|
|
1695
|
+
}
|
|
1696
|
+
if (route.queryDefaults && Object.keys(route.queryDefaults).length > 0) {
|
|
1697
|
+
fields.push(`queryDefaults: ${JSON.stringify(route.queryDefaults)}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (route.title) {
|
|
1700
|
+
fields.push(`title: ${JSON.stringify(route.title)}`);
|
|
1701
|
+
}
|
|
1702
|
+
if (route.data && Object.keys(route.data).length > 0) {
|
|
1703
|
+
fields.push(`data: ${JSON.stringify(route.data)}`);
|
|
1704
|
+
}
|
|
1705
|
+
const invokerArgs = (route.handlerParams ?? []).map((hp) => {
|
|
1706
|
+
if (hp.kind === "param") {
|
|
1707
|
+
const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
|
|
1708
|
+
const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
|
|
1709
|
+
if (hp.transform === "number") {
|
|
1710
|
+
return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
|
|
1711
|
+
}
|
|
1712
|
+
if (hp.transform === "boolean") {
|
|
1713
|
+
return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
|
|
1714
|
+
}
|
|
1715
|
+
if (hp.transform === "string") {
|
|
1716
|
+
return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
|
|
1717
|
+
}
|
|
1718
|
+
return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
|
|
1719
|
+
}
|
|
1720
|
+
if (hp.kind === "query") {
|
|
1721
|
+
const accessor = `req.query?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
|
|
1722
|
+
const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
|
|
1723
|
+
if (hp.transform === "number") {
|
|
1724
|
+
return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
|
|
1725
|
+
}
|
|
1726
|
+
if (hp.transform === "boolean") {
|
|
1727
|
+
return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
|
|
1728
|
+
}
|
|
1729
|
+
if (hp.transform === "string") {
|
|
1730
|
+
return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
|
|
1731
|
+
}
|
|
1732
|
+
return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
|
|
1733
|
+
}
|
|
1734
|
+
if (hp.kind === "body")
|
|
1735
|
+
return "req.body";
|
|
1736
|
+
if (hp.kind === "headers")
|
|
1737
|
+
return "req.headers";
|
|
1738
|
+
if (hp.kind === "context")
|
|
1739
|
+
return "(req.context ?? req)";
|
|
1740
|
+
return "undefined";
|
|
1741
|
+
});
|
|
1742
|
+
const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
|
|
1743
|
+
fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
|
|
861
1744
|
return `{ ${fields.join(", ")} }`;
|
|
862
1745
|
});
|
|
863
1746
|
return [
|
|
@@ -903,11 +1786,24 @@ ${indent(item, 2)}`).join(",")}
|
|
|
903
1786
|
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
904
1787
|
const lines = [];
|
|
905
1788
|
const returns = new Map;
|
|
1789
|
+
const multiGroups = new Map;
|
|
906
1790
|
for (const provider of providers) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1791
|
+
if (provider.multi) {
|
|
1792
|
+
const emitted = this.emitProvider(provider, kind, true);
|
|
1793
|
+
if (emitted.constLine)
|
|
1794
|
+
lines.push(emitted.constLine);
|
|
1795
|
+
const list = multiGroups.get(emitted.key) ?? [];
|
|
1796
|
+
list.push(emitted.expr);
|
|
1797
|
+
multiGroups.set(emitted.key, list);
|
|
1798
|
+
} else {
|
|
1799
|
+
const emitted = this.emitProvider(provider, kind, false);
|
|
1800
|
+
if (emitted.constLine)
|
|
1801
|
+
lines.push(emitted.constLine);
|
|
1802
|
+
returns.set(emitted.key, emitted.expr);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
for (const [key, exprs] of multiGroups) {
|
|
1806
|
+
returns.set(key, `[${exprs.join(", ")}]`);
|
|
911
1807
|
}
|
|
912
1808
|
for (const controller of controllers) {
|
|
913
1809
|
const emitted = this.emitController(controller, kind);
|
|
@@ -919,34 +1815,40 @@ ${indent(item, 2)}`).join(",")}
|
|
|
919
1815
|
return lines.join(`
|
|
920
1816
|
`);
|
|
921
1817
|
}
|
|
922
|
-
emitProvider(provider, kind) {
|
|
1818
|
+
emitProvider(provider, kind, isMulti = false) {
|
|
923
1819
|
const key = camelName(provider.token);
|
|
924
1820
|
switch (provider.kind) {
|
|
925
1821
|
case "class": {
|
|
926
1822
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
|
|
927
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
928
|
-
const local = this.localVar(provider.token, kind);
|
|
1823
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
|
|
1824
|
+
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
929
1825
|
return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
|
|
930
1826
|
}
|
|
931
1827
|
case "value": {
|
|
932
1828
|
const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
|
|
933
|
-
const local = this.localVar(provider.token, kind);
|
|
1829
|
+
const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
934
1830
|
return { constLine: `const ${local} = ${expr};`, key, expr: local };
|
|
935
1831
|
}
|
|
936
1832
|
case "factory": {
|
|
1833
|
+
if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
|
|
1834
|
+
const tokenIdent = this.imports.add(provider.token, provider.importPath);
|
|
1835
|
+
const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
1836
|
+
const constLine = `const ${local2} = typeof ${tokenIdent} === "object" && ${tokenIdent} && "factory" in ${tokenIdent} && typeof (${tokenIdent} as any).factory === "function" ? (${tokenIdent} as any).factory() : undefined;`;
|
|
1837
|
+
return { constLine, key, expr: local2 };
|
|
1838
|
+
}
|
|
937
1839
|
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
|
|
938
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
939
|
-
const local = this.localVar(provider.token, kind);
|
|
1840
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
|
|
1841
|
+
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
940
1842
|
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
941
1843
|
}
|
|
942
1844
|
case "existing": {
|
|
943
|
-
return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind) };
|
|
1845
|
+
return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
|
|
944
1846
|
}
|
|
945
1847
|
}
|
|
946
1848
|
}
|
|
947
1849
|
emitController(controller, kind) {
|
|
948
1850
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
949
|
-
const args = controller.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
1851
|
+
const args = controller.deps.map((dep) => this.depExpr(dep, kind, controller.optionalDeps?.includes(dep))).join(", ");
|
|
950
1852
|
const key = camelName(controller.className);
|
|
951
1853
|
const local = this.localVar(controller.className, kind);
|
|
952
1854
|
return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
|
|
@@ -966,7 +1868,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
966
1868
|
locals.set(token, local);
|
|
967
1869
|
return local;
|
|
968
1870
|
}
|
|
969
|
-
depExpr(token, kind) {
|
|
1871
|
+
depExpr(token, kind, isOptional = false) {
|
|
970
1872
|
if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
|
|
971
1873
|
return "ctx";
|
|
972
1874
|
if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
|
|
@@ -977,7 +1879,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
977
1879
|
return this.locals[kind].get(token) ?? camelName(token);
|
|
978
1880
|
}
|
|
979
1881
|
if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
|
|
980
|
-
return this.depExpr(own.useExisting ?? token, kind);
|
|
1882
|
+
return this.depExpr(own.useExisting ?? token, kind, isOptional);
|
|
981
1883
|
}
|
|
982
1884
|
if (kind === "services") {
|
|
983
1885
|
return `services.${camelName(token)}`;
|
|
@@ -992,9 +1894,18 @@ ${indent(item, 2)}`).join(",")}
|
|
|
992
1894
|
return `imported.${importName}.${camelName(token)}`;
|
|
993
1895
|
return `imported.${importName}.${camelName(token)}`;
|
|
994
1896
|
}
|
|
1897
|
+
for (const mod of this.graph.modules) {
|
|
1898
|
+
const rootProv = mod.providers.find((p) => p.token === token && p.providedIn === "root");
|
|
1899
|
+
if (rootProv) {
|
|
1900
|
+
return `imported.${mod.name}.${camelName(token)}`;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (isOptional && !this.graph.externalTokens.includes(token)) {
|
|
1904
|
+
return "undefined";
|
|
1905
|
+
}
|
|
995
1906
|
if (kind === "services")
|
|
996
|
-
return `deps.${camelName(token)}`;
|
|
997
|
-
return `services.${camelName(token)}`;
|
|
1907
|
+
return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
|
|
1908
|
+
return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
|
|
998
1909
|
}
|
|
999
1910
|
}
|
|
1000
1911
|
function orderProviders(providers) {
|
|
@@ -1016,6 +1927,226 @@ function orderProviders(providers) {
|
|
|
1016
1927
|
}
|
|
1017
1928
|
return result;
|
|
1018
1929
|
}
|
|
1930
|
+
function renderClient(graph, _options) {
|
|
1931
|
+
const controllerEntries = [];
|
|
1932
|
+
const allRoutes = [];
|
|
1933
|
+
for (const module of graph.modules) {
|
|
1934
|
+
for (const controller of module.controllers) {
|
|
1935
|
+
const controllerKey = camelName(controller.className.replace(/Controller$/, ""));
|
|
1936
|
+
const routeMethods = [];
|
|
1937
|
+
for (const route of controller.routes) {
|
|
1938
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1939
|
+
allRoutes.push({
|
|
1940
|
+
method: route.method,
|
|
1941
|
+
path: fullPath,
|
|
1942
|
+
controller: controller.className,
|
|
1943
|
+
handler: route.handler,
|
|
1944
|
+
command: route.command,
|
|
1945
|
+
guards: route.guards,
|
|
1946
|
+
canMatch: route.canMatch,
|
|
1947
|
+
canDeactivate: route.canDeactivate,
|
|
1948
|
+
resolvers: route.resolvers,
|
|
1949
|
+
redirectTo: route.redirectTo,
|
|
1950
|
+
pathMatch: route.pathMatch,
|
|
1951
|
+
paramTransforms: route.paramTransforms,
|
|
1952
|
+
paramDefaults: route.paramDefaults,
|
|
1953
|
+
queryTransforms: route.queryTransforms,
|
|
1954
|
+
queryDefaults: route.queryDefaults,
|
|
1955
|
+
title: route.title,
|
|
1956
|
+
data: route.data
|
|
1957
|
+
});
|
|
1958
|
+
routeMethods.push(`
|
|
1959
|
+
${route.handler}: (options: {
|
|
1960
|
+
params${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : "?"}: ${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? `{ ${(route.pathParams && route.pathParams.length > 0 ? route.pathParams : (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).map((p) => p.slice(1))).map((p) => `${p}: string | number`).join("; ")} }` : "Record<string, string | number>"};
|
|
1961
|
+
query?: Record<string, unknown>;
|
|
1962
|
+
body?: unknown;
|
|
1963
|
+
headers?: Record<string, string>;
|
|
1964
|
+
}${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : " = {}"}) => request(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, options),`);
|
|
1965
|
+
}
|
|
1966
|
+
controllerEntries.push(`
|
|
1967
|
+
${controllerKey}: {${routeMethods.join("")}
|
|
1968
|
+
},`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
return [
|
|
1972
|
+
HEADER,
|
|
1973
|
+
"",
|
|
1974
|
+
"export interface ClientRequestOptions {",
|
|
1975
|
+
" params?: Record<string, string | number>;",
|
|
1976
|
+
" query?: Record<string, unknown>;",
|
|
1977
|
+
" body?: unknown;",
|
|
1978
|
+
" headers?: Record<string, string>;",
|
|
1979
|
+
"}",
|
|
1980
|
+
"",
|
|
1981
|
+
"export type HttpInterceptorFn = (",
|
|
1982
|
+
" req: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
1983
|
+
" next: (req: { method: string; url: string; headers: Record<string, string>; body?: unknown }) => Promise<Response>,",
|
|
1984
|
+
") => Promise<Response>;",
|
|
1985
|
+
"",
|
|
1986
|
+
"export interface ApiClientConfig {",
|
|
1987
|
+
" baseUrl?: string;",
|
|
1988
|
+
" fetch?: typeof fetch;",
|
|
1989
|
+
" headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);",
|
|
1990
|
+
" interceptors?: HttpInterceptorFn[];",
|
|
1991
|
+
"}",
|
|
1992
|
+
"",
|
|
1993
|
+
"export const API_ROUTES = " + JSON.stringify(allRoutes, null, 2) + " as const;",
|
|
1994
|
+
"",
|
|
1995
|
+
"export type AppRoutePath = typeof API_ROUTES[number]['path'];",
|
|
1996
|
+
"",
|
|
1997
|
+
"/**",
|
|
1998
|
+
" * Type-safe URL builder replacing route path parameters and appending query parameters.",
|
|
1999
|
+
" */",
|
|
2000
|
+
"export function buildRouteUrl(",
|
|
2001
|
+
" path: string,",
|
|
2002
|
+
" params?: Record<string, string | number>,",
|
|
2003
|
+
" query?: Record<string, unknown>,",
|
|
2004
|
+
"): string {",
|
|
2005
|
+
" let url = path;",
|
|
2006
|
+
" if (params) {",
|
|
2007
|
+
" for (const [key, value] of Object.entries(params)) {",
|
|
2008
|
+
" url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
|
|
2009
|
+
" }",
|
|
2010
|
+
" }",
|
|
2011
|
+
" if (query) {",
|
|
2012
|
+
" const searchParams = new URLSearchParams();",
|
|
2013
|
+
" for (const [k, v] of Object.entries(query)) {",
|
|
2014
|
+
" if (v !== undefined && v !== null) searchParams.set(k, String(v));",
|
|
2015
|
+
" }",
|
|
2016
|
+
" const qs = searchParams.toString();",
|
|
2017
|
+
' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
|
|
2018
|
+
" }",
|
|
2019
|
+
" return url;",
|
|
2020
|
+
"}",
|
|
2021
|
+
"",
|
|
2022
|
+
"export function createApiClient(config: ApiClientConfig = {}) {",
|
|
2023
|
+
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
2024
|
+
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
2025
|
+
"",
|
|
2026
|
+
" async function request<T = unknown>(",
|
|
2027
|
+
" method: string,",
|
|
2028
|
+
" path: string,",
|
|
2029
|
+
" options: ClientRequestOptions = {},",
|
|
2030
|
+
" ): Promise<T> {",
|
|
2031
|
+
" let url = `${baseUrl}${path}`;",
|
|
2032
|
+
" if (options.params) {",
|
|
2033
|
+
" for (const [key, value] of Object.entries(options.params)) {",
|
|
2034
|
+
" url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
|
|
2035
|
+
" }",
|
|
2036
|
+
" }",
|
|
2037
|
+
" if (options.query) {",
|
|
2038
|
+
" const searchParams = new URLSearchParams();",
|
|
2039
|
+
" for (const [k, v] of Object.entries(options.query)) {",
|
|
2040
|
+
" if (v !== undefined && v !== null) searchParams.set(k, String(v));",
|
|
2041
|
+
" }",
|
|
2042
|
+
" const qs = searchParams.toString();",
|
|
2043
|
+
' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
|
|
2044
|
+
" }",
|
|
2045
|
+
' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
|
|
2046
|
+
" const headers: Record<string, string> = {",
|
|
2047
|
+
' "content-type": "application/json",',
|
|
2048
|
+
" ...customHeaders,",
|
|
2049
|
+
" ...options.headers,",
|
|
2050
|
+
" };",
|
|
2051
|
+
" const interceptors = config.interceptors ?? [];",
|
|
2052
|
+
" const executeChain = (",
|
|
2053
|
+
" index: number,",
|
|
2054
|
+
" reqPayload: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
2055
|
+
" ): Promise<Response> => {",
|
|
2056
|
+
" if (index < interceptors.length) {",
|
|
2057
|
+
" return interceptors[index](reqPayload, (nextPayload) => executeChain(index + 1, nextPayload));",
|
|
2058
|
+
" }",
|
|
2059
|
+
" return fetcher(reqPayload.url, {",
|
|
2060
|
+
" method: reqPayload.method,",
|
|
2061
|
+
" headers: reqPayload.headers,",
|
|
2062
|
+
" body: reqPayload.body !== undefined ? JSON.stringify(reqPayload.body) : undefined,",
|
|
2063
|
+
" });",
|
|
2064
|
+
" };",
|
|
2065
|
+
" const response = await executeChain(0, { method, url, headers, body: options.body });",
|
|
2066
|
+
" if (!response.ok) {",
|
|
2067
|
+
" const errBody = await response.text();",
|
|
2068
|
+
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
2069
|
+
" }",
|
|
2070
|
+
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
2071
|
+
' if (contentType.includes("application/json")) {',
|
|
2072
|
+
" return response.json() as Promise<T>;",
|
|
2073
|
+
" }",
|
|
2074
|
+
" return response.text() as Promise<T>;",
|
|
2075
|
+
" }",
|
|
2076
|
+
"",
|
|
2077
|
+
" return {",
|
|
2078
|
+
" request,",
|
|
2079
|
+
" buildRouteUrl,",
|
|
2080
|
+
" routes: API_ROUTES,",
|
|
2081
|
+
...controllerEntries,
|
|
2082
|
+
" };",
|
|
2083
|
+
"}",
|
|
2084
|
+
"",
|
|
2085
|
+
"export type ApiClient = ReturnType<typeof createApiClient>;",
|
|
2086
|
+
""
|
|
2087
|
+
].join(`
|
|
2088
|
+
`);
|
|
2089
|
+
}
|
|
2090
|
+
function renderPermissions(graph) {
|
|
2091
|
+
const permissions = new Set;
|
|
2092
|
+
const bindings = [];
|
|
2093
|
+
for (const module of graph.modules) {
|
|
2094
|
+
for (const command of module.commands) {
|
|
2095
|
+
if (command.permission)
|
|
2096
|
+
permissions.add(command.permission);
|
|
2097
|
+
}
|
|
2098
|
+
for (const controller of module.controllers) {
|
|
2099
|
+
for (const route of controller.routes) {
|
|
2100
|
+
let perm;
|
|
2101
|
+
if (route.command) {
|
|
2102
|
+
const cmd = module.commands.find((c) => c.className === route.command);
|
|
2103
|
+
perm = cmd?.permission;
|
|
2104
|
+
}
|
|
2105
|
+
if (perm)
|
|
2106
|
+
permissions.add(perm);
|
|
2107
|
+
bindings.push({
|
|
2108
|
+
method: route.method,
|
|
2109
|
+
path: joinRoutePaths(controller.path, route.path),
|
|
2110
|
+
controller: controller.className,
|
|
2111
|
+
handler: route.handler,
|
|
2112
|
+
command: route.command,
|
|
2113
|
+
permission: perm
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
const sortedPerms = [...permissions].sort();
|
|
2119
|
+
const enumEntries = sortedPerms.map((perm) => {
|
|
2120
|
+
const key = pascalName(perm.replace(/[^A-Za-z0-9]+/g, " "));
|
|
2121
|
+
return ` ${key}: ${JSON.stringify(perm)},`;
|
|
2122
|
+
});
|
|
2123
|
+
return [
|
|
2124
|
+
HEADER,
|
|
2125
|
+
"",
|
|
2126
|
+
"export const AppPermissions = {",
|
|
2127
|
+
...enumEntries,
|
|
2128
|
+
"} as const;",
|
|
2129
|
+
"",
|
|
2130
|
+
"export type AppPermission = (typeof AppPermissions)[keyof typeof AppPermissions];",
|
|
2131
|
+
"",
|
|
2132
|
+
"export interface RoutePermissionBinding {",
|
|
2133
|
+
" method: string;",
|
|
2134
|
+
" path: string;",
|
|
2135
|
+
" controller: string;",
|
|
2136
|
+
" handler: string;",
|
|
2137
|
+
" command?: string;",
|
|
2138
|
+
" permission?: string;",
|
|
2139
|
+
"}",
|
|
2140
|
+
"",
|
|
2141
|
+
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
2142
|
+
"",
|
|
2143
|
+
"export function hasPermission(granted: string[], required: AppPermission | string): boolean {",
|
|
2144
|
+
' return granted.includes("*") || granted.includes(required);',
|
|
2145
|
+
"}",
|
|
2146
|
+
""
|
|
2147
|
+
].join(`
|
|
2148
|
+
`);
|
|
2149
|
+
}
|
|
1019
2150
|
|
|
1020
2151
|
// src/profiles.ts
|
|
1021
2152
|
var MODULAR_MONOLITH_RULES = [
|
|
@@ -1190,6 +2321,53 @@ var SCOPE_LIFETIME_RANK = {
|
|
|
1190
2321
|
request: 1,
|
|
1191
2322
|
job: 1
|
|
1192
2323
|
};
|
|
2324
|
+
var COMPILER_DIAGNOSTIC_CODES = {
|
|
2325
|
+
"circular-dependency": { code: "SC1001", docsUrl: "https://supacloud.dev/errors/SC1001" },
|
|
2326
|
+
"scope-violation": { code: "SC1002", docsUrl: "https://supacloud.dev/errors/SC1002" },
|
|
2327
|
+
"module-boundary-violation": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
|
|
2328
|
+
"module-boundary": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
|
|
2329
|
+
"circular-module-import": { code: "SC1004", docsUrl: "https://supacloud.dev/errors/SC1004" },
|
|
2330
|
+
"orphan-module": { code: "SC1005", docsUrl: "https://supacloud.dev/errors/SC1005" },
|
|
2331
|
+
"invalid-boundary-preset": { code: "SC1006", docsUrl: "https://supacloud.dev/errors/SC1006" },
|
|
2332
|
+
"circular-existing-alias": { code: "SC1007", docsUrl: "https://supacloud.dev/errors/SC1007" },
|
|
2333
|
+
"missing-deps": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
|
|
2334
|
+
"unresolved-token": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
|
|
2335
|
+
"duplicate-token": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
|
|
2336
|
+
"duplicate-module": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
|
|
2337
|
+
"disallow-controller-direct-db": { code: "SC2003", docsUrl: "https://supacloud.dev/errors/SC2003" },
|
|
2338
|
+
"self-dependency-violation": { code: "SC2004", docsUrl: "https://supacloud.dev/errors/SC2004" },
|
|
2339
|
+
"skip-self-dependency-violation": { code: "SC2005", docsUrl: "https://supacloud.dev/errors/SC2005" },
|
|
2340
|
+
"export-unprovided-token": { code: "SC2006", docsUrl: "https://supacloud.dev/errors/SC2006" },
|
|
2341
|
+
"unresolved-alias-target": { code: "SC2007", docsUrl: "https://supacloud.dev/errors/SC2007" },
|
|
2342
|
+
"self-referencing-alias": { code: "SC2008", docsUrl: "https://supacloud.dev/errors/SC2008" },
|
|
2343
|
+
"shadowed-route": { code: "SC3001", docsUrl: "https://supacloud.dev/errors/SC3001" },
|
|
2344
|
+
"unresolved-route-redirect": { code: "SC3002", docsUrl: "https://supacloud.dev/errors/SC3002" },
|
|
2345
|
+
"circular-route-redirect": { code: "SC3003", docsUrl: "https://supacloud.dev/errors/SC3003" },
|
|
2346
|
+
"invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
|
|
2347
|
+
"unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
|
|
2348
|
+
"missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
2349
|
+
"duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
|
|
2350
|
+
"missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
|
|
2351
|
+
"unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
|
|
2352
|
+
"malformed-route-path": { code: "SC3010", docsUrl: "https://supacloud.dev/errors/SC3010" },
|
|
2353
|
+
"duplicate-path-param": { code: "SC3011", docsUrl: "https://supacloud.dev/errors/SC3011" },
|
|
2354
|
+
"wildcard-not-trailing": { code: "SC3012", docsUrl: "https://supacloud.dev/errors/SC3012" },
|
|
2355
|
+
"invalid-query-param-name": { code: "SC3013", docsUrl: "https://supacloud.dev/errors/SC3013" },
|
|
2356
|
+
"unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
|
|
2357
|
+
"invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
|
|
2358
|
+
"disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
2359
|
+
"duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
|
|
2360
|
+
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
2361
|
+
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
2362
|
+
"missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
|
|
2363
|
+
"command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
|
|
2364
|
+
"duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
|
|
2365
|
+
"route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
|
|
2366
|
+
"command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
|
|
2367
|
+
"route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
|
|
2368
|
+
"command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
|
|
2369
|
+
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
2370
|
+
};
|
|
1193
2371
|
function validateGraph(graph, options = false) {
|
|
1194
2372
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
1195
2373
|
const diagnostics = [];
|
|
@@ -1201,12 +2379,15 @@ function validateGraph(graph, options = false) {
|
|
|
1201
2379
|
rules: options.moduleBoundaries
|
|
1202
2380
|
});
|
|
1203
2381
|
} catch (err) {
|
|
2382
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["invalid-boundary-preset"];
|
|
1204
2383
|
diagnostics.push({
|
|
1205
2384
|
severity: "error",
|
|
1206
2385
|
code: "invalid-boundary-preset",
|
|
1207
2386
|
message: err instanceof Error ? err.message : String(err),
|
|
1208
2387
|
file: graph.modules[0]?.file,
|
|
1209
|
-
line: graph.modules[0]?.line
|
|
2388
|
+
line: graph.modules[0]?.line,
|
|
2389
|
+
errorCode: meta?.code,
|
|
2390
|
+
docsUrl: meta?.docsUrl
|
|
1210
2391
|
});
|
|
1211
2392
|
}
|
|
1212
2393
|
}
|
|
@@ -1230,17 +2411,43 @@ function validateGraph(graph, options = false) {
|
|
|
1230
2411
|
if (provider)
|
|
1231
2412
|
return { module: imported, provider };
|
|
1232
2413
|
}
|
|
2414
|
+
for (const mod of graph.modules) {
|
|
2415
|
+
const rootProvider = mod.providers.find((p) => p.token === token && p.providedIn === "root");
|
|
2416
|
+
if (rootProvider)
|
|
2417
|
+
return { module: mod, provider: rootProvider };
|
|
2418
|
+
}
|
|
1233
2419
|
return;
|
|
1234
2420
|
}
|
|
1235
|
-
const error = (code, message, file, line) => {
|
|
1236
|
-
|
|
2421
|
+
const error = (code, message, file, line, suggestion) => {
|
|
2422
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
2423
|
+
diagnostics.push({
|
|
2424
|
+
severity: "error",
|
|
2425
|
+
code,
|
|
2426
|
+
message,
|
|
2427
|
+
file,
|
|
2428
|
+
line,
|
|
2429
|
+
suggestion,
|
|
2430
|
+
errorCode: meta?.code,
|
|
2431
|
+
docsUrl: meta?.docsUrl
|
|
2432
|
+
});
|
|
1237
2433
|
};
|
|
1238
|
-
const warn2 = (code, message, file, line) => {
|
|
1239
|
-
|
|
2434
|
+
const warn2 = (code, message, file, line, suggestion) => {
|
|
2435
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
2436
|
+
diagnostics.push({
|
|
2437
|
+
severity: strict ? "error" : "warn",
|
|
2438
|
+
code,
|
|
2439
|
+
message,
|
|
2440
|
+
file,
|
|
2441
|
+
line,
|
|
2442
|
+
suggestion,
|
|
2443
|
+
errorCode: meta?.code,
|
|
2444
|
+
docsUrl: meta?.docsUrl
|
|
2445
|
+
});
|
|
1240
2446
|
};
|
|
1241
2447
|
const modulesByName = new Map;
|
|
1242
2448
|
const commandsByName = new Map;
|
|
1243
2449
|
const routesByKey = new Map;
|
|
2450
|
+
const declaredRoutes = [];
|
|
1244
2451
|
for (const module of graph.modules) {
|
|
1245
2452
|
const previousModule = modulesByName.get(module.name);
|
|
1246
2453
|
if (previousModule) {
|
|
@@ -1260,20 +2467,128 @@ function validateGraph(graph, options = false) {
|
|
|
1260
2467
|
for (const module of graph.modules) {
|
|
1261
2468
|
for (const controller of module.controllers) {
|
|
1262
2469
|
for (const route of controller.routes) {
|
|
1263
|
-
|
|
2470
|
+
if (route.path.includes("//") || controller.path.includes("//")) {
|
|
2471
|
+
error("malformed-route-path", `Route ${route.method} ${route.path} has malformed path: contains consecutive slashes '//'.`, controller.file, undefined, "Remove duplicate consecutive slashes from the route path.");
|
|
2472
|
+
} else if (/(^|\/):(\/|$)/.test(route.path) || route.path.endsWith("/:")) {
|
|
2473
|
+
error("malformed-route-path", `Route ${route.method} ${route.path} has malformed path: parameter colon ':' is missing a parameter identifier.`, controller.file, undefined, "Specify a valid parameter name following the colon (e.g. ':id').");
|
|
2474
|
+
} else if (route.path.includes("?") || route.path.includes("#")) {
|
|
2475
|
+
error("malformed-route-path", `Route ${route.method} ${route.path} has malformed path: contains invalid URL query '?' or fragment '#' character.`, controller.file, undefined, "Declare query parameters using @Query() decorators instead of in the route path.");
|
|
2476
|
+
}
|
|
2477
|
+
if (route.path.includes("**")) {
|
|
2478
|
+
const segments = route.path.split("/").filter(Boolean);
|
|
2479
|
+
const wildcardIdx = segments.indexOf("**");
|
|
2480
|
+
if (wildcardIdx !== -1 && wildcardIdx !== segments.length - 1) {
|
|
2481
|
+
error("wildcard-not-trailing", `Route ${route.method} '${route.path}' defines wildcard '**' in the middle of the path. In Angular Router semantics, wildcard '**' must be the trailing segment.`, controller.file, undefined, `Move the wildcard '**' to the end of the route path, e.g. '${segments.slice(0, wildcardIdx).join("/")}/**'.`);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const fullPath = joinRoutePaths2(controller.path, route.path);
|
|
2485
|
+
const rawFullPath = joinRawRoutePaths(controller.path, route.path);
|
|
1264
2486
|
const key = `${route.method} ${fullPath}`;
|
|
2487
|
+
const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
|
|
2488
|
+
if (openApiMatch) {
|
|
2489
|
+
error("missing-param-colon", `Route path '${route.path}' in '${controller.className}.${route.handler}' uses OpenAPI-style '{${openApiMatch[1]}}'. SupaCloud routes require Express/Angular-style ':${openApiMatch[1]}'.`, controller.file, undefined, `Replace '{${openApiMatch[1]}}' with ':${openApiMatch[1]}'.`);
|
|
2490
|
+
}
|
|
1265
2491
|
const previous = routesByKey.get(key);
|
|
1266
2492
|
if (previous) {
|
|
1267
2493
|
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
1268
2494
|
} else {
|
|
1269
2495
|
routesByKey.set(key, { module, controller });
|
|
1270
2496
|
}
|
|
2497
|
+
for (const prev of declaredRoutes) {
|
|
2498
|
+
if (prev.method === route.method && isRouteShadowed(prev.rawFullPath, rawFullPath)) {
|
|
2499
|
+
warn2("shadowed-route", `Route ${route.method} ${rawFullPath} (${controller.className}.${route.handler}) is shadowed by earlier parameterized route ${prev.method} ${prev.rawFullPath} (${prev.controller.className}.${prev.handler}) and will never be matched.`, controller.file, undefined, `Move specific route '${route.path}' before parameterized route '${prev.path}'.`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
declaredRoutes.push({ method: route.method, path: route.path, fullPath, rawFullPath, controller, module, handler: route.handler, redirectTo: route.redirectTo });
|
|
1271
2503
|
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
1272
2504
|
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
1273
2505
|
}
|
|
2506
|
+
if ((route.method === "GET" || route.method === "HEAD") && route.command) {
|
|
2507
|
+
const boundCommand = module.commands.find((c) => c.className === route.command);
|
|
2508
|
+
if (boundCommand && boundCommand.transaction === "required") {
|
|
2509
|
+
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${route.command}' with transaction: 'required'. Mutating transactions are not permitted on read-only HTTP GET requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for mutating command routes, or set transaction: 'none'.`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
1274
2512
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
1275
2513
|
error("route-command-binding-disallowed", `Route ${key} binds command ${route.command}, but route-level command bindings are disabled by policy. Use an application service (${controller.className}.${route.handler}, ${controller.file}).`, controller.file);
|
|
1276
2514
|
}
|
|
2515
|
+
if (route.redirectTo) {
|
|
2516
|
+
const target = route.redirectTo.replace(/\/+$/, "");
|
|
2517
|
+
const current = fullPath.replace(/\/+$/, "");
|
|
2518
|
+
if (target === current || target === route.path.replace(/\/+$/, "")) {
|
|
2519
|
+
error("circular-route-redirect", `Route ${key} defines circular redirectTo '${route.redirectTo}'`, controller.file);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const pathParams = route.pathParams ?? [];
|
|
2523
|
+
const seenParams = new Set;
|
|
2524
|
+
for (const p of pathParams) {
|
|
2525
|
+
if (seenParams.has(p)) {
|
|
2526
|
+
error("duplicate-path-param", `Route ${route.method} '${route.path}' defines duplicate path parameter ':${p}'. Each parameter in a route path must be unique.`, controller.file, undefined, `Rename the duplicate parameter ':${p}' to a unique name (e.g. ':${p}Id').`);
|
|
2527
|
+
}
|
|
2528
|
+
seenParams.add(p);
|
|
2529
|
+
}
|
|
2530
|
+
const paramBindings = route.paramBindings ?? [];
|
|
2531
|
+
for (const binding of paramBindings) {
|
|
2532
|
+
if (!binding || binding.trim().length === 0) {
|
|
2533
|
+
error("unmatched-path-param-decorator", `Controller ${controller.className} handler ${route.handler} specifies an empty @Param() parameter binding.`, controller.file, undefined, `Specify a non-empty path parameter name matching a segment in route path '${route.path}'.`);
|
|
2534
|
+
} else if (/[#?&=/\s]/.test(binding)) {
|
|
2535
|
+
error("unmatched-path-param-decorator", `Controller ${controller.className} handler ${route.handler} specifies invalid @Param('${binding}') with illegal character. Path parameter names cannot contain '#', '?', '&', '=', '/', or whitespace.`, controller.file, undefined, `Rename path parameter binding '${binding}' to a valid identifier matching route path segment.`);
|
|
2536
|
+
} else if (!pathParams.includes(binding)) {
|
|
2537
|
+
const suggestion = findClosestMatch(binding, pathParams);
|
|
2538
|
+
error("unmatched-path-param", `Controller ${controller.className} handler ${route.handler} binds @Param('${binding}'), but route path '${route.path}' does not define parameter ':${binding}'.`, controller.file, undefined, suggestion ? `Did you mean @Param('${suggestion}')?` : undefined);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (paramBindings.length > 0) {
|
|
2542
|
+
for (const param of pathParams) {
|
|
2543
|
+
if (!paramBindings.includes(param)) {
|
|
2544
|
+
warn2("missing-path-param", `Route path '${route.path}' defines parameter ':${param}', but handler ${controller.className}.${route.handler} does not bind it with @Param('${param}').`, controller.file, undefined, `Add @Param('${param}') to ${route.handler} arguments.`);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const queryBindings = route.queryBindings ?? [];
|
|
2549
|
+
const seenQueries = new Set;
|
|
2550
|
+
for (const q of queryBindings) {
|
|
2551
|
+
if (!q || q.trim().length === 0) {
|
|
2552
|
+
error("invalid-query-param-name", `Controller ${controller.className} handler ${route.handler} specifies an empty @Query() parameter binding.`, controller.file, undefined, `Specify a non-empty parameter name in @Query('paramName').`);
|
|
2553
|
+
} else if (/[#?&=/\s]/.test(q)) {
|
|
2554
|
+
error("invalid-query-param-name", `Controller ${controller.className} handler ${route.handler} specifies invalid @Query('${q}') with illegal character. Query parameter names cannot contain '#', '?', '&', '=', '/', or whitespace.`, controller.file, undefined, `Rename query parameter '${q}' to a valid identifier name without reserved characters.`);
|
|
2555
|
+
} else if (seenQueries.has(q)) {
|
|
2556
|
+
error("duplicate-query-param-binding", `Controller ${controller.className} handler ${route.handler} specifies duplicate @Query('${q}') parameter binding. Each query parameter should only be bound once per handler.`, controller.file, undefined, `Remove or rename the duplicate @Query('${q}') parameter binding in ${route.handler}.`);
|
|
2557
|
+
}
|
|
2558
|
+
seenQueries.add(q);
|
|
2559
|
+
}
|
|
2560
|
+
if (route.queryDefaults && route.queryTransforms) {
|
|
2561
|
+
for (const [paramName, defVal] of Object.entries(route.queryDefaults)) {
|
|
2562
|
+
const transform = route.queryTransforms[paramName];
|
|
2563
|
+
if (transform === "number" && typeof defVal !== "number") {
|
|
2564
|
+
error("invalid-query-default-type", `Controller ${controller.className} handler ${route.handler} specifies transform 'number' for @Query('${paramName}'), but default value '${String(defVal)}' is not a number.`, controller.file, undefined, `Provide a numeric default (e.g. default: 0) or change transform type to 'string'.`);
|
|
2565
|
+
} else if (transform === "boolean" && typeof defVal !== "boolean") {
|
|
2566
|
+
error("invalid-query-default-type", `Controller ${controller.className} handler ${route.handler} specifies transform 'boolean' for @Query('${paramName}'), but default value '${String(defVal)}' is not a boolean.`, controller.file, undefined, `Provide a boolean default (e.g. default: false) or change transform type.`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
|
|
2571
|
+
error("disallowed-body-on-get-delete", `Route handler ${controller.className}.${route.handler} binds @Body() or declares body schema on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`);
|
|
2572
|
+
}
|
|
2573
|
+
if (route.hasBodyBinding && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
2574
|
+
error("invalid-body-binding", `Route handler ${controller.className}.${route.handler} binds @Body() on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`);
|
|
2575
|
+
} else if (route.hasBodyBinding && !route.body) {
|
|
2576
|
+
warn2("missing-body-schema", `Route handler ${controller.className}.${route.handler} binds @Body() on route '${route.path}', but route definition does not specify a body validation schema.`, controller.file, undefined, `Add schema to route options (e.g. body: Schema) for compile-time and runtime validation.`);
|
|
2577
|
+
} else if (route.body && !route.hasBodyBinding && !route.command) {
|
|
2578
|
+
warn2("unused-route-schema", `Route '${route.path}' defines body schema '${route.body}', but handler ${controller.className}.${route.handler} does not bind @Body().`, controller.file, undefined, `Bind parameter with @Body() in ${controller.className}.${route.handler} or remove unused body schema option.`);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
const handlerMethodMap = new Map;
|
|
2582
|
+
for (const route of controller.routes) {
|
|
2583
|
+
const methods = handlerMethodMap.get(route.handler) ?? [];
|
|
2584
|
+
methods.push(route.method);
|
|
2585
|
+
handlerMethodMap.set(route.handler, methods);
|
|
2586
|
+
}
|
|
2587
|
+
for (const [handler, methods] of handlerMethodMap.entries()) {
|
|
2588
|
+
const uniqueMethods = Array.from(new Set(methods));
|
|
2589
|
+
if (uniqueMethods.length > 1) {
|
|
2590
|
+
warn2("conflicting-route-method", `Controller ${controller.className} handler '${handler}' is mapped to multiple HTTP methods: ${uniqueMethods.join(", ")}.`, controller.file, undefined, `Separate distinct HTTP methods into separate controller handlers.`);
|
|
2591
|
+
}
|
|
1277
2592
|
}
|
|
1278
2593
|
if (typeof options === "object" && options.disallowControllerDirectDb) {
|
|
1279
2594
|
for (const dep of controller.deps) {
|
|
@@ -1283,6 +2598,73 @@ function validateGraph(graph, options = false) {
|
|
|
1283
2598
|
}
|
|
1284
2599
|
}
|
|
1285
2600
|
}
|
|
2601
|
+
if (controller.selfDeps && controller.selfDeps.length > 0) {
|
|
2602
|
+
for (const dep of controller.selfDeps) {
|
|
2603
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2604
|
+
if (!own) {
|
|
2605
|
+
error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
|
|
2610
|
+
for (const dep of controller.skipSelfDeps) {
|
|
2611
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2612
|
+
if (own) {
|
|
2613
|
+
error("skip-self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, controller.file, undefined, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const allTargetPaths = declaredRoutes.map((r) => r.rawFullPath);
|
|
2620
|
+
for (const item of declaredRoutes) {
|
|
2621
|
+
if (item.redirectTo) {
|
|
2622
|
+
const target = item.redirectTo;
|
|
2623
|
+
if (target.startsWith("/") && !target.startsWith("//")) {
|
|
2624
|
+
const normalizedTarget = target.replace(/\/+$/, "") || "/";
|
|
2625
|
+
const matchesTarget = declaredRoutes.some((candidate) => {
|
|
2626
|
+
if (candidate.rawFullPath === normalizedTarget)
|
|
2627
|
+
return true;
|
|
2628
|
+
return routeMatchesTarget(candidate.rawFullPath, normalizedTarget);
|
|
2629
|
+
});
|
|
2630
|
+
if (!matchesTarget) {
|
|
2631
|
+
const suggestion = findClosestMatch(normalizedTarget, allTargetPaths);
|
|
2632
|
+
warn2("unresolved-route-redirect", `Route ${item.method} ${item.rawFullPath} (${item.controller.className}.${item.handler}) redirects to '${target}', but no matching route was found in the application graph.`, item.controller.file, undefined, suggestion ? `Did you mean '${suggestion}'?` : undefined);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const routeByRawPath = new Map;
|
|
2638
|
+
for (const item of declaredRoutes) {
|
|
2639
|
+
if (!routeByRawPath.has(item.rawFullPath)) {
|
|
2640
|
+
routeByRawPath.set(item.rawFullPath, item);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const reportedRedirectCycles = new Set;
|
|
2644
|
+
for (const item of declaredRoutes) {
|
|
2645
|
+
if (item.redirectTo) {
|
|
2646
|
+
const chain = [item.rawFullPath];
|
|
2647
|
+
let curr = item;
|
|
2648
|
+
while (curr && curr.redirectTo) {
|
|
2649
|
+
const target = curr.redirectTo.replace(/\/+$/, "") || "/";
|
|
2650
|
+
if (chain.includes(target)) {
|
|
2651
|
+
const cycle = [...chain.slice(chain.indexOf(target)), target];
|
|
2652
|
+
if (cycle.length > 2) {
|
|
2653
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
2654
|
+
if (!reportedRedirectCycles.has(cycleKey)) {
|
|
2655
|
+
reportedRedirectCycles.add(cycleKey);
|
|
2656
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-route-redirect"];
|
|
2657
|
+
error("circular-route-redirect", `Route redirect chain forms a cycle: ${cycle.join(" -> ")}`, item.controller.file, undefined, "Break the redirect loop by terminating at a concrete non-redirect route.");
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
chain.push(target);
|
|
2663
|
+
const next = routeByRawPath.get(target);
|
|
2664
|
+
if (!next || !next.redirectTo)
|
|
2665
|
+
break;
|
|
2666
|
+
curr = next;
|
|
2667
|
+
}
|
|
1286
2668
|
}
|
|
1287
2669
|
}
|
|
1288
2670
|
for (const module of graph.modules) {
|
|
@@ -1290,33 +2672,58 @@ function validateGraph(graph, options = false) {
|
|
|
1290
2672
|
for (const provider of module.providers) {
|
|
1291
2673
|
const first = seen.get(provider.token);
|
|
1292
2674
|
if (first) {
|
|
1293
|
-
|
|
2675
|
+
if (first.multi && provider.multi) {
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
error("duplicate-token", `模块 ${module.name} 重复注册 token ${provider.token}(首次注册于 ${first.file}:${first.line})`, provider.file, provider.line, "If multiple providers are intended for this token, specify 'multi: true' on each provider definition (Angular multi-providers pattern).");
|
|
1294
2679
|
} else {
|
|
1295
2680
|
seen.set(provider.token, provider);
|
|
1296
2681
|
}
|
|
1297
2682
|
}
|
|
1298
2683
|
for (const provider of module.providers) {
|
|
2684
|
+
if (provider.selfDeps && provider.selfDeps.length > 0) {
|
|
2685
|
+
for (const dep of provider.selfDeps) {
|
|
2686
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2687
|
+
if (!own) {
|
|
2688
|
+
error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
|
|
2693
|
+
for (const dep of provider.skipSelfDeps) {
|
|
2694
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2695
|
+
if (own) {
|
|
2696
|
+
error("skip-self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, provider.file, provider.line, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
1299
2700
|
for (const dep of provider.deps) {
|
|
2701
|
+
const isOptional = provider.optionalDeps?.includes(dep);
|
|
1300
2702
|
const resolved = resolveDep(module, dep);
|
|
1301
2703
|
if (!resolved) {
|
|
2704
|
+
if (isOptional) {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
1302
2707
|
if (!graph.externalTokens.includes(dep)) {
|
|
1303
2708
|
if (globalProviders.has(dep)) {
|
|
1304
2709
|
const owner = globalProviders.get(dep);
|
|
1305
|
-
error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line);
|
|
2710
|
+
error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line, `Import module '${owner.module.name}' in '${module.name}', add '${dep}' to '${owner.module.name}' exports, or mark @Injectable({ providedIn: 'root' }).`);
|
|
2711
|
+
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
2712
|
+
error("missing-token-factory", `InjectionToken '${dep}' referenced by provider '${provider.token}' has no provider in module '${module.name}' and no default factory function.`, provider.file, provider.line, `Provide '${dep}' in @Module({ providers: [...] }) or declare it with new InjectionToken('${dep}', { factory: () => ... }).`);
|
|
1306
2713
|
} else {
|
|
1307
|
-
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line);
|
|
2714
|
+
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line, `Provide '${dep}' in a module, mark constructor parameter @Optional(), or define @Injectable({ providedIn: 'root' }).`);
|
|
1308
2715
|
}
|
|
1309
2716
|
}
|
|
1310
2717
|
continue;
|
|
1311
2718
|
}
|
|
1312
2719
|
if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
|
|
1313
|
-
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line);
|
|
2720
|
+
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line, `Change provider '${provider.token}' scope to '${resolved.provider.scope}', or inject a factory/context instead.`);
|
|
1314
2721
|
}
|
|
1315
2722
|
}
|
|
1316
2723
|
}
|
|
1317
2724
|
for (const command of module.commands) {
|
|
1318
2725
|
if (!command.permission) {
|
|
1319
|
-
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
2726
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line, "Add 'permission: string' to @Command({ ... }) or configure command execution capabilities permission=false.");
|
|
1320
2727
|
}
|
|
1321
2728
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
1322
2729
|
const caps = options.commandCapabilities;
|
|
@@ -1369,18 +2776,116 @@ function validateGraph(graph, options = false) {
|
|
|
1369
2776
|
}
|
|
1370
2777
|
}
|
|
1371
2778
|
}
|
|
2779
|
+
const referencedTokens = new Set;
|
|
2780
|
+
for (const mod of graph.modules) {
|
|
2781
|
+
for (const exp of mod.exports)
|
|
2782
|
+
referencedTokens.add(exp);
|
|
2783
|
+
for (const ctrl of mod.controllers) {
|
|
2784
|
+
for (const d of ctrl.deps ?? [])
|
|
2785
|
+
referencedTokens.add(d);
|
|
2786
|
+
for (const d of ctrl.optionalDeps ?? [])
|
|
2787
|
+
referencedTokens.add(d);
|
|
2788
|
+
for (const d of ctrl.selfDeps ?? [])
|
|
2789
|
+
referencedTokens.add(d);
|
|
2790
|
+
for (const d of ctrl.skipSelfDeps ?? [])
|
|
2791
|
+
referencedTokens.add(d);
|
|
2792
|
+
}
|
|
2793
|
+
for (const p of mod.providers) {
|
|
2794
|
+
for (const d of p.deps ?? [])
|
|
2795
|
+
referencedTokens.add(d);
|
|
2796
|
+
for (const d of p.optionalDeps ?? [])
|
|
2797
|
+
referencedTokens.add(d);
|
|
2798
|
+
for (const d of p.selfDeps ?? [])
|
|
2799
|
+
referencedTokens.add(d);
|
|
2800
|
+
for (const d of p.skipSelfDeps ?? [])
|
|
2801
|
+
referencedTokens.add(d);
|
|
2802
|
+
if (p.useExisting)
|
|
2803
|
+
referencedTokens.add(p.useExisting);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
for (const mod of graph.modules) {
|
|
2807
|
+
for (const provider of mod.providers) {
|
|
2808
|
+
if (provider.providedIn === "root" && !provider.multi && !referencedTokens.has(provider.token) && !provider.exported) {
|
|
2809
|
+
warn2("unused-root-provider", `Root provider "${provider.token}" is declared with providedIn: 'root' but is never injected or depended on by any module, controller, or command.`, provider.file, provider.line, `Inject "${provider.token}" in a service or controller, export it, or remove providedIn: 'root' to enable tree-shaking.`);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
for (const module of graph.modules) {
|
|
2814
|
+
for (const expToken of module.exports) {
|
|
2815
|
+
const resolved = resolveDep(module, expToken);
|
|
2816
|
+
if (resolved)
|
|
2817
|
+
continue;
|
|
2818
|
+
if (module.imports.includes(expToken))
|
|
2819
|
+
continue;
|
|
2820
|
+
error("export-unprovided-token", `Module '${module.name}' exports token '${expToken}', but it is neither provided in '${module.name}' nor imported from an imported module.`, module.file, module.line, `Add a provider for '${expToken}' to '${module.name}.providers', or remove '${expToken}' from exports.`);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
for (const module of graph.modules) {
|
|
2824
|
+
for (const provider of module.providers) {
|
|
2825
|
+
if (provider.useExisting) {
|
|
2826
|
+
const target = provider.useExisting;
|
|
2827
|
+
if (target === provider.token) {
|
|
2828
|
+
error("self-referencing-alias", `Module '${module.name}' defines provider '${provider.token}' with useExisting referencing itself.`, provider.file ?? module.file, provider.line ?? module.line, `Change useExisting to reference a different provider token, or remove the self-referencing alias.`);
|
|
2829
|
+
} else {
|
|
2830
|
+
const resolved = resolveDep(module, target);
|
|
2831
|
+
if (!resolved && !graph.externalTokens.includes(target)) {
|
|
2832
|
+
error("unresolved-alias-target", `Module '${module.name}' defines provider '${provider.token}' with useExisting: '${target}', but '${target}' is neither provided in '${module.name}' nor imported from an imported module.`, provider.file ?? module.file, provider.line ?? module.line, `Add a provider for '${target}' to '${module.name}.providers' or an imported module, or update useExisting to reference an available token.`);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
1372
2838
|
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
2839
|
+
diagnostics.push(...detectExistingAliasCycles(graph, resolveDep));
|
|
1373
2840
|
diagnostics.push(...detectModuleCycles(graph));
|
|
1374
2841
|
if (typeof options === "object" && options.detectOrphanModules) {
|
|
1375
2842
|
diagnostics.push(...detectOrphanModules(graph));
|
|
1376
2843
|
}
|
|
1377
2844
|
return diagnostics;
|
|
1378
2845
|
}
|
|
1379
|
-
function
|
|
2846
|
+
function joinRoutePaths2(prefix, path) {
|
|
1380
2847
|
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1381
2848
|
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1382
2849
|
return normalized.replace(/:[^/]+/g, ":param");
|
|
1383
2850
|
}
|
|
2851
|
+
function joinRawRoutePaths(prefix, path) {
|
|
2852
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
2853
|
+
return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
2854
|
+
}
|
|
2855
|
+
function isRouteShadowed(earlierPath, laterPath) {
|
|
2856
|
+
const earlierSegments = earlierPath.split("/").filter(Boolean);
|
|
2857
|
+
const laterSegments = laterPath.split("/").filter(Boolean);
|
|
2858
|
+
if (earlierSegments.length !== laterSegments.length) {
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
let hasParamShadowing = false;
|
|
2862
|
+
for (let i = 0;i < earlierSegments.length; i += 1) {
|
|
2863
|
+
const e = earlierSegments[i];
|
|
2864
|
+
const l = laterSegments[i];
|
|
2865
|
+
if (e === l) {
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
if (e.startsWith(":") && !l.startsWith(":")) {
|
|
2869
|
+
hasParamShadowing = true;
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
return hasParamShadowing;
|
|
2875
|
+
}
|
|
2876
|
+
function routeMatchesTarget(routePattern, targetPath) {
|
|
2877
|
+
const pSegs = routePattern.split("/").filter(Boolean);
|
|
2878
|
+
const tSegs = targetPath.split("/").filter(Boolean);
|
|
2879
|
+
if (pSegs.length !== tSegs.length)
|
|
2880
|
+
return false;
|
|
2881
|
+
for (let i = 0;i < pSegs.length; i += 1) {
|
|
2882
|
+
if (pSegs[i].startsWith(":"))
|
|
2883
|
+
continue;
|
|
2884
|
+
if (pSegs[i] !== tSegs[i])
|
|
2885
|
+
return false;
|
|
2886
|
+
}
|
|
2887
|
+
return true;
|
|
2888
|
+
}
|
|
1384
2889
|
function detectCycles(graph, resolveDep) {
|
|
1385
2890
|
const diagnostics = [];
|
|
1386
2891
|
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
|
@@ -1399,12 +2904,16 @@ function detectCycles(graph, resolveDep) {
|
|
|
1399
2904
|
const cycleKey = cycle.map((item) => nodeId(item)).sort().join("|");
|
|
1400
2905
|
if (!reported.has(cycleKey)) {
|
|
1401
2906
|
reported.add(cycleKey);
|
|
2907
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-dependency"];
|
|
1402
2908
|
diagnostics.push({
|
|
1403
2909
|
severity: "error",
|
|
1404
2910
|
code: "circular-dependency",
|
|
1405
2911
|
message: `provider 循环依赖: ${path}`,
|
|
1406
2912
|
file: ref.provider.file,
|
|
1407
|
-
line: ref.provider.line
|
|
2913
|
+
line: ref.provider.line,
|
|
2914
|
+
suggestion: "Break the cycle by extracting common dependencies into a separate service or injecting @Optional().",
|
|
2915
|
+
errorCode: meta?.code,
|
|
2916
|
+
docsUrl: meta?.docsUrl
|
|
1408
2917
|
});
|
|
1409
2918
|
}
|
|
1410
2919
|
return;
|
|
@@ -1423,6 +2932,47 @@ function detectCycles(graph, resolveDep) {
|
|
|
1423
2932
|
visit(ref);
|
|
1424
2933
|
return diagnostics;
|
|
1425
2934
|
}
|
|
2935
|
+
function detectExistingAliasCycles(graph, resolveDep) {
|
|
2936
|
+
const diagnostics = [];
|
|
2937
|
+
const existingProviders = [];
|
|
2938
|
+
for (const module of graph.modules) {
|
|
2939
|
+
for (const provider of module.providers) {
|
|
2940
|
+
if (provider.useExisting) {
|
|
2941
|
+
existingProviders.push({ module, provider });
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const reported = new Set;
|
|
2946
|
+
for (const start of existingProviders) {
|
|
2947
|
+
const visited = [start.provider.token];
|
|
2948
|
+
let current = start;
|
|
2949
|
+
while (current && current.provider.useExisting) {
|
|
2950
|
+
const targetToken = current.provider.useExisting;
|
|
2951
|
+
if (visited.includes(targetToken)) {
|
|
2952
|
+
const cycle = [...visited.slice(visited.indexOf(targetToken)), targetToken];
|
|
2953
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
2954
|
+
if (!reported.has(cycleKey)) {
|
|
2955
|
+
reported.add(cycleKey);
|
|
2956
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-existing-alias"];
|
|
2957
|
+
diagnostics.push({
|
|
2958
|
+
severity: "error",
|
|
2959
|
+
code: "circular-existing-alias",
|
|
2960
|
+
message: `Provider alias cycle detected in useExisting: ${cycle.join(" -> ")}`,
|
|
2961
|
+
file: start.provider.file,
|
|
2962
|
+
line: start.provider.line,
|
|
2963
|
+
suggestion: "Break the alias cycle by pointing useExisting to a concrete provider instead of a circular alias.",
|
|
2964
|
+
errorCode: meta?.code,
|
|
2965
|
+
docsUrl: meta?.docsUrl
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
break;
|
|
2969
|
+
}
|
|
2970
|
+
visited.push(targetToken);
|
|
2971
|
+
current = resolveDep(current.module, targetToken);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return diagnostics;
|
|
2975
|
+
}
|
|
1426
2976
|
function detectModuleCycles(graph) {
|
|
1427
2977
|
const diagnostics = [];
|
|
1428
2978
|
const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
|
|
@@ -1439,12 +2989,16 @@ function detectModuleCycles(graph) {
|
|
|
1439
2989
|
if (!reported.has(cycleKey)) {
|
|
1440
2990
|
reported.add(cycleKey);
|
|
1441
2991
|
const mod2 = moduleMap.get(name);
|
|
2992
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-module-import"];
|
|
1442
2993
|
diagnostics.push({
|
|
1443
2994
|
severity: "error",
|
|
1444
2995
|
code: "circular-module-import",
|
|
1445
2996
|
message: `Module circular import detected: ${cycle.join(" -> ")}`,
|
|
1446
2997
|
file: mod2?.file,
|
|
1447
|
-
line: mod2?.line
|
|
2998
|
+
line: mod2?.line,
|
|
2999
|
+
suggestion: "Refactor module imports into a unidirectional acyclic graph.",
|
|
3000
|
+
errorCode: meta?.code,
|
|
3001
|
+
docsUrl: meta?.docsUrl
|
|
1448
3002
|
});
|
|
1449
3003
|
}
|
|
1450
3004
|
return;
|
|
@@ -1492,12 +3046,15 @@ function detectOrphanModules(graph) {
|
|
|
1492
3046
|
}
|
|
1493
3047
|
for (const mod of graph.modules) {
|
|
1494
3048
|
if (!reachable.has(mod.name)) {
|
|
3049
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["orphan-module"];
|
|
1495
3050
|
diagnostics.push({
|
|
1496
3051
|
severity: "warn",
|
|
1497
3052
|
code: "orphan-module",
|
|
1498
3053
|
message: `Module '${mod.name}' is declared but not reachable from any root module (${rootModules.map((r) => r.name).join(", ")})`,
|
|
1499
3054
|
file: mod.file,
|
|
1500
|
-
line: mod.line
|
|
3055
|
+
line: mod.line,
|
|
3056
|
+
errorCode: meta?.code,
|
|
3057
|
+
docsUrl: meta?.docsUrl
|
|
1501
3058
|
});
|
|
1502
3059
|
}
|
|
1503
3060
|
}
|
|
@@ -1508,7 +3065,7 @@ function detectOrphanModules(graph) {
|
|
|
1508
3065
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1509
3066
|
import { join as join3 } from "node:path";
|
|
1510
3067
|
async function compileProject(options) {
|
|
1511
|
-
const graph = await analyzeProject(options.rootDir, options.include);
|
|
3068
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
1512
3069
|
const diagnostics = [
|
|
1513
3070
|
...graph.diagnostics ?? [],
|
|
1514
3071
|
...validateGraph(graph, {
|
|
@@ -1527,14 +3084,25 @@ async function compileProject(options) {
|
|
|
1527
3084
|
diagnostic.severity = "error";
|
|
1528
3085
|
}
|
|
1529
3086
|
}
|
|
1530
|
-
const
|
|
3087
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
3088
|
+
const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
|
|
1531
3089
|
rootDir: options.rootDir,
|
|
1532
|
-
outDir: options.outDir
|
|
1533
|
-
|
|
1534
|
-
|
|
3090
|
+
outDir: options.outDir,
|
|
3091
|
+
generateClient: options.generateClient,
|
|
3092
|
+
generatePermissions: options.generatePermissions,
|
|
3093
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
3094
|
+
}) : [];
|
|
3095
|
+
const stats = graph.cacheStats ? {
|
|
3096
|
+
cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
|
|
3097
|
+
changedFiles: [],
|
|
3098
|
+
affectedModules: graph.cacheStats.reanalyzedModules,
|
|
3099
|
+
reanalyzedModules: graph.cacheStats.reanalyzedModules,
|
|
3100
|
+
reusedModules: graph.cacheStats.reusedModules
|
|
3101
|
+
} : undefined;
|
|
3102
|
+
return { diagnostics, graph, written, stats };
|
|
1535
3103
|
}
|
|
1536
3104
|
async function checkProject(options) {
|
|
1537
|
-
const graph = await analyzeProject(options.rootDir, options.include);
|
|
3105
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
1538
3106
|
const diagnostics = [
|
|
1539
3107
|
...graph.diagnostics ?? [],
|
|
1540
3108
|
...validateGraph(graph, {
|
|
@@ -1555,12 +3123,21 @@ async function checkProject(options) {
|
|
|
1555
3123
|
}
|
|
1556
3124
|
const rendered = renderApplication(graph, {
|
|
1557
3125
|
rootDir: options.rootDir,
|
|
1558
|
-
outDir: options.outDir
|
|
3126
|
+
outDir: options.outDir,
|
|
3127
|
+
generateClient: options.generateClient,
|
|
3128
|
+
generatePermissions: options.generatePermissions,
|
|
3129
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
1559
3130
|
});
|
|
1560
3131
|
const expectedFiles = {
|
|
1561
3132
|
"application.ts": rendered.applicationCode,
|
|
1562
3133
|
"app.manifest.json": rendered.manifestJson
|
|
1563
3134
|
};
|
|
3135
|
+
if (rendered.clientCode) {
|
|
3136
|
+
expectedFiles["client.ts"] = rendered.clientCode;
|
|
3137
|
+
}
|
|
3138
|
+
if (rendered.permissionsCode) {
|
|
3139
|
+
expectedFiles["permissions.ts"] = rendered.permissionsCode;
|
|
3140
|
+
}
|
|
1564
3141
|
const mismatches = [];
|
|
1565
3142
|
for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
|
|
1566
3143
|
const diskPath = join3(options.outDir, filename);
|
|
@@ -1580,19 +3157,530 @@ async function checkProject(options) {
|
|
|
1580
3157
|
graph
|
|
1581
3158
|
};
|
|
1582
3159
|
}
|
|
3160
|
+
// src/watch.ts
|
|
3161
|
+
import { watch } from "node:fs";
|
|
3162
|
+
import { relative as relative3, resolve as resolve2 } from "node:path";
|
|
3163
|
+
|
|
3164
|
+
// src/incremental.ts
|
|
3165
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3166
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
3167
|
+
import { relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
3168
|
+
function createDependencyGraphCache() {
|
|
3169
|
+
return {
|
|
3170
|
+
modules: new Map,
|
|
3171
|
+
fileHashes: new Map
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
function createIncrementalCompiler() {
|
|
3175
|
+
let previousSnapshot;
|
|
3176
|
+
let previousResult;
|
|
3177
|
+
const cache = createDependencyGraphCache();
|
|
3178
|
+
return {
|
|
3179
|
+
async compile(options, changedPaths) {
|
|
3180
|
+
const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
|
|
3181
|
+
const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
|
|
3182
|
+
const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && changedFiles.length === 0);
|
|
3183
|
+
if (cacheHit && previousResult) {
|
|
3184
|
+
return {
|
|
3185
|
+
...previousResult,
|
|
3186
|
+
stats: {
|
|
3187
|
+
cacheHit: true,
|
|
3188
|
+
changedFiles: [],
|
|
3189
|
+
affectedModules: [],
|
|
3190
|
+
reusedModules: previousResult.graph.modules.map((m) => m.name),
|
|
3191
|
+
reanalyzedModules: []
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
if (previousResult && previousSnapshot && changedFiles.length > 0 && !await requiresGraphRebuild(options.rootDir, changedFiles)) {
|
|
3196
|
+
const stats2 = {
|
|
3197
|
+
cacheHit: true,
|
|
3198
|
+
changedFiles,
|
|
3199
|
+
affectedModules: findAffectedModules(previousResult.graph.modules, previousResult.graph.modules, changedFiles),
|
|
3200
|
+
reusedModules: previousResult.graph.modules.map((m) => m.name),
|
|
3201
|
+
reanalyzedModules: []
|
|
3202
|
+
};
|
|
3203
|
+
previousSnapshot = snapshot;
|
|
3204
|
+
return { ...previousResult, written: [], stats: stats2 };
|
|
3205
|
+
}
|
|
3206
|
+
const activeCache = options.cache ?? cache;
|
|
3207
|
+
if (!activeCache.dependencyGraph && previousResult) {
|
|
3208
|
+
activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
|
|
3209
|
+
}
|
|
3210
|
+
const result = await compileProject({ ...options, cache: activeCache });
|
|
3211
|
+
const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
|
|
3212
|
+
const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
|
|
3213
|
+
const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
|
|
3214
|
+
if (activeCache) {
|
|
3215
|
+
activeCache.dependencyGraph = new ModuleDependencyGraph(result.graph.modules);
|
|
3216
|
+
}
|
|
3217
|
+
const stats = {
|
|
3218
|
+
cacheHit: false,
|
|
3219
|
+
changedFiles,
|
|
3220
|
+
affectedModules,
|
|
3221
|
+
reusedModules,
|
|
3222
|
+
reanalyzedModules
|
|
3223
|
+
};
|
|
3224
|
+
previousSnapshot = snapshot;
|
|
3225
|
+
previousResult = result;
|
|
3226
|
+
return { ...result, stats };
|
|
3227
|
+
},
|
|
3228
|
+
reset() {
|
|
3229
|
+
previousSnapshot = undefined;
|
|
3230
|
+
previousResult = undefined;
|
|
3231
|
+
cache.modules.clear();
|
|
3232
|
+
cache.fileHashes.clear();
|
|
3233
|
+
cache.dependencyGraph = undefined;
|
|
3234
|
+
},
|
|
3235
|
+
getCache() {
|
|
3236
|
+
return cache;
|
|
3237
|
+
}
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
async function updateSnapshot(previous, options, changedPaths) {
|
|
3241
|
+
const rootDir = resolve(options.rootDir);
|
|
3242
|
+
const outDir = resolve(options.outDir);
|
|
3243
|
+
const files = { ...previous.files };
|
|
3244
|
+
for (const changedPath of changedPaths) {
|
|
3245
|
+
const absolutePath = resolve(rootDir, changedPath);
|
|
3246
|
+
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
3247
|
+
continue;
|
|
3248
|
+
const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
|
|
3249
|
+
try {
|
|
3250
|
+
await access(absolutePath);
|
|
3251
|
+
const content = await readFile(absolutePath);
|
|
3252
|
+
files[relativePath] = createHash2("sha256").update(content).digest("hex");
|
|
3253
|
+
} catch {
|
|
3254
|
+
delete files[relativePath];
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
return { files, optionsKey: optionsKeyOf(options) };
|
|
3258
|
+
}
|
|
3259
|
+
async function createSnapshot(options) {
|
|
3260
|
+
const rootDir = resolve(options.rootDir);
|
|
3261
|
+
const outDir = resolve(options.outDir);
|
|
3262
|
+
const paths = await listSourceFiles(rootDir, outDir);
|
|
3263
|
+
const files = {};
|
|
3264
|
+
for (const path of paths) {
|
|
3265
|
+
const content = await readFile(path);
|
|
3266
|
+
files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
|
|
3267
|
+
}
|
|
3268
|
+
return { files, optionsKey: optionsKeyOf(options) };
|
|
3269
|
+
}
|
|
3270
|
+
function optionsKeyOf(options) {
|
|
3271
|
+
return JSON.stringify({
|
|
3272
|
+
include: options.include,
|
|
3273
|
+
strict: options.strict,
|
|
3274
|
+
moduleBoundaryPreset: options.moduleBoundaryPreset,
|
|
3275
|
+
moduleBoundaries: options.moduleBoundaries,
|
|
3276
|
+
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
3277
|
+
commandCapabilities: options.commandCapabilities,
|
|
3278
|
+
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
3279
|
+
detectOrphanModules: options.detectOrphanModules,
|
|
3280
|
+
generateClient: options.generateClient,
|
|
3281
|
+
generatePermissions: options.generatePermissions
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
async function requiresGraphRebuild(rootDir, changedFiles) {
|
|
3285
|
+
for (const relativePath of changedFiles) {
|
|
3286
|
+
const path = resolve(rootDir, relativePath);
|
|
3287
|
+
try {
|
|
3288
|
+
const source = await readFile(path, "utf8");
|
|
3289
|
+
if (/@(?:Module|Injectable|Inject|Controller|Command|Query)\b|new\s+InjectionToken\b|\bdefineModule\s*\(/.test(source))
|
|
3290
|
+
return true;
|
|
3291
|
+
} catch {
|
|
3292
|
+
return true;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return false;
|
|
3296
|
+
}
|
|
3297
|
+
async function listSourceFiles(rootDir, outDir) {
|
|
3298
|
+
const result = [];
|
|
3299
|
+
const visit = async (directory) => {
|
|
3300
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
3301
|
+
const path = resolve(directory, entry.name);
|
|
3302
|
+
if (entry.isDirectory()) {
|
|
3303
|
+
if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
|
|
3304
|
+
continue;
|
|
3305
|
+
await visit(path);
|
|
3306
|
+
} else if (/\.(tsx?|mts|cts)$/.test(entry.name)) {
|
|
3307
|
+
result.push(path);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
await visit(rootDir);
|
|
3312
|
+
return result.sort();
|
|
3313
|
+
}
|
|
3314
|
+
function diffFiles(previous, current) {
|
|
3315
|
+
if (!previous)
|
|
3316
|
+
return Object.keys(current);
|
|
3317
|
+
const names = new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
3318
|
+
return [...names].filter((name) => previous[name] !== current[name]).sort();
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
class ModuleDependencyGraph {
|
|
3322
|
+
imports = new Map;
|
|
3323
|
+
dependents = new Map;
|
|
3324
|
+
fileOwners = new Map;
|
|
3325
|
+
moduleMap = new Map;
|
|
3326
|
+
constructor(modules = []) {
|
|
3327
|
+
this.rebuild(modules);
|
|
3328
|
+
}
|
|
3329
|
+
rebuild(modules) {
|
|
3330
|
+
this.imports.clear();
|
|
3331
|
+
this.dependents.clear();
|
|
3332
|
+
this.fileOwners.clear();
|
|
3333
|
+
this.moduleMap.clear();
|
|
3334
|
+
for (const mod of modules) {
|
|
3335
|
+
this.moduleMap.set(mod.name, mod);
|
|
3336
|
+
this.imports.set(mod.name, new Set(mod.imports));
|
|
3337
|
+
if (!this.dependents.has(mod.name)) {
|
|
3338
|
+
this.dependents.set(mod.name, new Set);
|
|
3339
|
+
}
|
|
3340
|
+
this.indexFile(mod.file, mod.name);
|
|
3341
|
+
for (const p of mod.providers) {
|
|
3342
|
+
if (p.importPath)
|
|
3343
|
+
this.indexFile(p.importPath, mod.name);
|
|
3344
|
+
if (p.file)
|
|
3345
|
+
this.indexFile(p.file, mod.name);
|
|
3346
|
+
}
|
|
3347
|
+
for (const c of mod.controllers) {
|
|
3348
|
+
if (c.importPath)
|
|
3349
|
+
this.indexFile(c.importPath, mod.name);
|
|
3350
|
+
if (c.file)
|
|
3351
|
+
this.indexFile(c.file, mod.name);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
for (const [modName, imps] of this.imports.entries()) {
|
|
3355
|
+
for (const imp of imps) {
|
|
3356
|
+
if (!this.dependents.has(imp)) {
|
|
3357
|
+
this.dependents.set(imp, new Set);
|
|
3358
|
+
}
|
|
3359
|
+
this.dependents.get(imp).add(modName);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
indexFile(path, moduleName) {
|
|
3364
|
+
if (!path)
|
|
3365
|
+
return;
|
|
3366
|
+
const normalized = path.replace(/\.(tsx?|mts|cts)$/, "");
|
|
3367
|
+
if (!this.fileOwners.has(normalized)) {
|
|
3368
|
+
this.fileOwners.set(normalized, new Set);
|
|
3369
|
+
}
|
|
3370
|
+
this.fileOwners.get(normalized).add(moduleName);
|
|
3371
|
+
}
|
|
3372
|
+
getModulesOwningFile(filePath) {
|
|
3373
|
+
const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
|
|
3374
|
+
return Array.from(this.fileOwners.get(normalized) ?? []);
|
|
3375
|
+
}
|
|
3376
|
+
getAffectedModules(changedFiles) {
|
|
3377
|
+
if (changedFiles.length === 0)
|
|
3378
|
+
return [];
|
|
3379
|
+
const directlyAffected = new Set;
|
|
3380
|
+
for (const file of changedFiles) {
|
|
3381
|
+
for (const modName of this.getModulesOwningFile(file)) {
|
|
3382
|
+
directlyAffected.add(modName);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (directlyAffected.size === 0) {
|
|
3386
|
+
return Array.from(this.moduleMap.keys());
|
|
3387
|
+
}
|
|
3388
|
+
const affected = new Set(directlyAffected);
|
|
3389
|
+
const queue = Array.from(directlyAffected);
|
|
3390
|
+
while (queue.length > 0) {
|
|
3391
|
+
const current = queue.shift();
|
|
3392
|
+
const dependents = this.dependents.get(current);
|
|
3393
|
+
if (dependents) {
|
|
3394
|
+
for (const dep of dependents) {
|
|
3395
|
+
if (!affected.has(dep)) {
|
|
3396
|
+
affected.add(dep);
|
|
3397
|
+
queue.push(dep);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
return Array.from(this.moduleMap.keys()).filter((name) => affected.has(name));
|
|
3403
|
+
}
|
|
3404
|
+
getDirectImports(moduleName) {
|
|
3405
|
+
return Array.from(this.imports.get(moduleName) ?? []);
|
|
3406
|
+
}
|
|
3407
|
+
getDirectDependents(moduleName) {
|
|
3408
|
+
return Array.from(this.dependents.get(moduleName) ?? []);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function findAffectedModules(previous, current, changedFiles) {
|
|
3412
|
+
const depGraph = new ModuleDependencyGraph(current);
|
|
3413
|
+
return depGraph.getAffectedModules(changedFiles);
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
// src/watch.ts
|
|
3417
|
+
var DEFAULT_DEBOUNCE_MS = 100;
|
|
3418
|
+
function watchProject(options) {
|
|
3419
|
+
const rootDir = resolve2(options.rootDir);
|
|
3420
|
+
const outDir = resolve2(options.outDir);
|
|
3421
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
3422
|
+
let timer;
|
|
3423
|
+
let closed = false;
|
|
3424
|
+
let compiling = false;
|
|
3425
|
+
let pending = false;
|
|
3426
|
+
const pendingPaths = new Set;
|
|
3427
|
+
let watcher;
|
|
3428
|
+
const incremental = createIncrementalCompiler();
|
|
3429
|
+
let initialEvent;
|
|
3430
|
+
let resolveReady;
|
|
3431
|
+
let rejectReady;
|
|
3432
|
+
const ready = new Promise((resolvePromise, rejectPromise) => {
|
|
3433
|
+
resolveReady = resolvePromise;
|
|
3434
|
+
rejectReady = rejectPromise;
|
|
3435
|
+
});
|
|
3436
|
+
const emit = (event) => {
|
|
3437
|
+
options.onEvent?.(event);
|
|
3438
|
+
if (event.initial)
|
|
3439
|
+
initialEvent = event;
|
|
3440
|
+
};
|
|
3441
|
+
const compile = async (initial, changedPaths = []) => {
|
|
3442
|
+
if (closed && !initial)
|
|
3443
|
+
return;
|
|
3444
|
+
if (compiling) {
|
|
3445
|
+
pending = true;
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
compiling = true;
|
|
3449
|
+
const startedAt = performance.now();
|
|
3450
|
+
options.onEvent?.({
|
|
3451
|
+
type: "compile-start",
|
|
3452
|
+
initial,
|
|
3453
|
+
durationMs: 0,
|
|
3454
|
+
diagnostics: [],
|
|
3455
|
+
written: []
|
|
3456
|
+
});
|
|
3457
|
+
try {
|
|
3458
|
+
const result = await incremental.compile({ ...options, writeOnError: false }, changedPaths);
|
|
3459
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
3460
|
+
const hasErrors = result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
3461
|
+
emit({
|
|
3462
|
+
type: hasErrors ? "compile-error" : "compiled",
|
|
3463
|
+
initial,
|
|
3464
|
+
durationMs,
|
|
3465
|
+
diagnostics: result.diagnostics,
|
|
3466
|
+
written: result.written,
|
|
3467
|
+
stats: result.stats
|
|
3468
|
+
});
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
rejectReady(error);
|
|
3471
|
+
throw error;
|
|
3472
|
+
} finally {
|
|
3473
|
+
compiling = false;
|
|
3474
|
+
if (pending && !closed) {
|
|
3475
|
+
pending = false;
|
|
3476
|
+
compile(false, [...pendingPaths]);
|
|
3477
|
+
pendingPaths.clear();
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
const schedule = (changedPath) => {
|
|
3482
|
+
if (closed)
|
|
3483
|
+
return;
|
|
3484
|
+
if (changedPath)
|
|
3485
|
+
pendingPaths.add(changedPath);
|
|
3486
|
+
if (timer)
|
|
3487
|
+
clearTimeout(timer);
|
|
3488
|
+
timer = setTimeout(() => {
|
|
3489
|
+
timer = undefined;
|
|
3490
|
+
compile(false, [...pendingPaths]);
|
|
3491
|
+
pendingPaths.clear();
|
|
3492
|
+
}, debounceMs);
|
|
3493
|
+
};
|
|
3494
|
+
compile(true).then(() => {
|
|
3495
|
+
if (closed)
|
|
3496
|
+
return;
|
|
3497
|
+
watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
3498
|
+
if (!filename)
|
|
3499
|
+
return schedule();
|
|
3500
|
+
const changedPath = resolve2(rootDir, filename.toString());
|
|
3501
|
+
const relativePath = relative3(outDir, changedPath);
|
|
3502
|
+
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
3503
|
+
return;
|
|
3504
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
3505
|
+
schedule(relative3(rootDir, changedPath));
|
|
3506
|
+
});
|
|
3507
|
+
if (initialEvent)
|
|
3508
|
+
resolveReady(initialEvent);
|
|
3509
|
+
}).catch(() => {
|
|
3510
|
+
return;
|
|
3511
|
+
});
|
|
3512
|
+
return {
|
|
3513
|
+
ready,
|
|
3514
|
+
async close() {
|
|
3515
|
+
closed = true;
|
|
3516
|
+
if (timer)
|
|
3517
|
+
clearTimeout(timer);
|
|
3518
|
+
watcher?.close();
|
|
3519
|
+
await ready.catch(() => {
|
|
3520
|
+
return;
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
// src/inspect.ts
|
|
3526
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
3527
|
+
import { join as join4 } from "node:path";
|
|
3528
|
+
function formatGraph(graph) {
|
|
3529
|
+
const lines = [];
|
|
3530
|
+
for (const module of graph.modules) {
|
|
3531
|
+
lines.push(`MODULE ${module.name}`);
|
|
3532
|
+
lines.push(` file: ${module.file}:${module.line}`);
|
|
3533
|
+
lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
|
|
3534
|
+
lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
|
|
3535
|
+
lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
|
|
3536
|
+
lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
|
|
3537
|
+
}
|
|
3538
|
+
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
3539
|
+
return lines.join(`
|
|
3540
|
+
`);
|
|
3541
|
+
}
|
|
3542
|
+
function explainGraph(graph, subject) {
|
|
3543
|
+
const module = graph.modules.find((candidate) => candidate.name === subject);
|
|
3544
|
+
if (module)
|
|
3545
|
+
return explainModule(graph, module);
|
|
3546
|
+
const provider = findProvider(graph, subject);
|
|
3547
|
+
if (provider)
|
|
3548
|
+
return explainProvider(graph, provider.module, provider.provider);
|
|
3549
|
+
if (graph.externalTokens.includes(subject)) {
|
|
3550
|
+
const references = graph.modules.flatMap((candidate) => [
|
|
3551
|
+
...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
|
|
3552
|
+
...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
|
|
3553
|
+
]);
|
|
3554
|
+
return [
|
|
3555
|
+
`EXTERNAL TOKEN ${subject}`,
|
|
3556
|
+
" provided by: platform runtime",
|
|
3557
|
+
` references: ${references.length > 0 ? references.join(", ") : "-"}`
|
|
3558
|
+
].join(`
|
|
3559
|
+
`);
|
|
3560
|
+
}
|
|
3561
|
+
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
3562
|
+
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
3563
|
+
}
|
|
3564
|
+
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
3565
|
+
const checks = [
|
|
3566
|
+
{
|
|
3567
|
+
name: "project-root",
|
|
3568
|
+
ok: existsSync3(rootDir),
|
|
3569
|
+
detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
name: "tsconfig",
|
|
3573
|
+
ok: existsSync3(join4(rootDir, "tsconfig.json")),
|
|
3574
|
+
detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
3575
|
+
},
|
|
3576
|
+
{
|
|
3577
|
+
name: "modules",
|
|
3578
|
+
ok: graph.modules.length > 0,
|
|
3579
|
+
detail: `${graph.modules.length} module(s) discovered`
|
|
3580
|
+
},
|
|
3581
|
+
{
|
|
3582
|
+
name: "generated-artifacts",
|
|
3583
|
+
ok: upToDate,
|
|
3584
|
+
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
3585
|
+
}
|
|
3586
|
+
];
|
|
3587
|
+
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
3588
|
+
return {
|
|
3589
|
+
checks,
|
|
3590
|
+
diagnostics: allDiagnostics,
|
|
3591
|
+
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
function explainModule(graph, module) {
|
|
3595
|
+
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
3596
|
+
return [
|
|
3597
|
+
`MODULE ${module.name}`,
|
|
3598
|
+
` file: ${module.file}:${module.line}`,
|
|
3599
|
+
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
3600
|
+
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
3601
|
+
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
3602
|
+
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
3603
|
+
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`
|
|
3604
|
+
].join(`
|
|
3605
|
+
`);
|
|
3606
|
+
}
|
|
3607
|
+
function explainProvider(graph, module, provider) {
|
|
3608
|
+
const dependents = graph.modules.flatMap((candidate) => [
|
|
3609
|
+
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
3610
|
+
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
3611
|
+
]);
|
|
3612
|
+
return [
|
|
3613
|
+
`PROVIDER ${provider.token}`,
|
|
3614
|
+
` module: ${module.name}`,
|
|
3615
|
+
` file: ${provider.file}:${provider.line}`,
|
|
3616
|
+
` kind: ${provider.kind}`,
|
|
3617
|
+
` scope: ${provider.scope}`,
|
|
3618
|
+
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
3619
|
+
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
3620
|
+
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
3621
|
+
].join(`
|
|
3622
|
+
`);
|
|
3623
|
+
}
|
|
3624
|
+
function findProvider(graph, subject) {
|
|
3625
|
+
for (const module of graph.modules) {
|
|
3626
|
+
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
3627
|
+
if (provider)
|
|
3628
|
+
return { module, provider };
|
|
3629
|
+
}
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
function exportGraphMermaid(graph) {
|
|
3633
|
+
const lines = ["graph TD"];
|
|
3634
|
+
for (const mod of graph.modules) {
|
|
3635
|
+
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3636
|
+
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
3637
|
+
for (const imp of mod.imports) {
|
|
3638
|
+
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3639
|
+
lines.push(` ${safeId} --> ${safeImp}`);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
return lines.join(`
|
|
3643
|
+
`);
|
|
3644
|
+
}
|
|
3645
|
+
function exportGraphDot(graph) {
|
|
3646
|
+
const lines = [
|
|
3647
|
+
"digraph ApplicationGraph {",
|
|
3648
|
+
" rankdir=LR;",
|
|
3649
|
+
' node [shape=box, fontname="Helvetica"];'
|
|
3650
|
+
];
|
|
3651
|
+
for (const mod of graph.modules) {
|
|
3652
|
+
lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
|
|
3653
|
+
for (const imp of mod.imports) {
|
|
3654
|
+
lines.push(` "${mod.name}" -> "${imp}";`);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
lines.push("}");
|
|
3658
|
+
return lines.join(`
|
|
3659
|
+
`);
|
|
3660
|
+
}
|
|
1583
3661
|
export {
|
|
1584
3662
|
ANGULAR_ENTERPRISE_RULES,
|
|
1585
3663
|
CLEAN_ARCHITECTURE_RULES,
|
|
3664
|
+
COMPILER_DIAGNOSTIC_CODES,
|
|
1586
3665
|
MODULAR_MONOLITH_RULES,
|
|
1587
3666
|
MODULE_BOUNDARY_PROFILES,
|
|
3667
|
+
ModuleDependencyGraph,
|
|
1588
3668
|
analyzeProject,
|
|
1589
3669
|
camelName,
|
|
1590
3670
|
checkProject,
|
|
1591
3671
|
compileProject,
|
|
3672
|
+
createDependencyGraphCache,
|
|
3673
|
+
createIncrementalCompiler,
|
|
3674
|
+
doctorProject,
|
|
3675
|
+
explainGraph,
|
|
3676
|
+
exportGraphDot,
|
|
3677
|
+
exportGraphMermaid,
|
|
3678
|
+
formatGraph,
|
|
1592
3679
|
generateApplication,
|
|
1593
3680
|
getModuleBoundaryPreset,
|
|
1594
3681
|
getModuleBoundaryProfile,
|
|
1595
3682
|
renderApplication,
|
|
1596
3683
|
resolveModuleBoundaries,
|
|
1597
|
-
validateGraph
|
|
3684
|
+
validateGraph,
|
|
3685
|
+
watchProject
|
|
1598
3686
|
};
|