@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/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { resolve as resolve3 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/analyze.ts
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
import { join, relative, sep } from "node:path";
|
|
9
10
|
import {
|
|
10
11
|
Node,
|
|
@@ -22,10 +23,20 @@ var ROUTE_DECORATORS = {
|
|
|
22
23
|
Options: "OPTIONS"
|
|
23
24
|
};
|
|
24
25
|
var SCOPES = ["application", "request", "job"];
|
|
25
|
-
async function analyzeProject(rootDir, include) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
async function analyzeProject(rootDir, include, cache) {
|
|
27
|
+
let project;
|
|
28
|
+
if (cache?.project) {
|
|
29
|
+
project = cache.project;
|
|
30
|
+
for (const sf of project.getSourceFiles()) {
|
|
31
|
+
sf.refreshFromFileSystemSync();
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
project = createProject(rootDir);
|
|
35
|
+
const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
|
|
36
|
+
project.addSourceFilesAtPaths(patterns);
|
|
37
|
+
if (cache)
|
|
38
|
+
cache.project = project;
|
|
39
|
+
}
|
|
29
40
|
const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
|
|
30
41
|
const ctx = {
|
|
31
42
|
rootDir,
|
|
@@ -75,7 +86,207 @@ async function analyzeProject(rootDir, include) {
|
|
|
75
86
|
for (const c of candidates) {
|
|
76
87
|
nameByNode.set(c.node, stringLiteralProp(c.options, "name") ?? c.className);
|
|
77
88
|
}
|
|
78
|
-
|
|
89
|
+
let modules = [];
|
|
90
|
+
let reusedModules = [];
|
|
91
|
+
let reanalyzedModules = [];
|
|
92
|
+
if (cache) {
|
|
93
|
+
const currentFileHashes = new Map;
|
|
94
|
+
for (const sf of sourceFiles) {
|
|
95
|
+
const rel = sourcePath(rootDir, sf.getFilePath());
|
|
96
|
+
const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
|
|
97
|
+
currentFileHashes.set(rel, hash);
|
|
98
|
+
}
|
|
99
|
+
const changedFiles = new Set;
|
|
100
|
+
for (const [file, hash] of currentFileHashes.entries()) {
|
|
101
|
+
if (cache.fileHashes.get(file) !== hash) {
|
|
102
|
+
changedFiles.add(file);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const file of cache.fileHashes.keys()) {
|
|
106
|
+
if (!currentFileHashes.has(file)) {
|
|
107
|
+
changedFiles.add(file);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const modulesToKeep = new Map;
|
|
111
|
+
const finalModules = [];
|
|
112
|
+
const finalDiagnostics = [];
|
|
113
|
+
const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
|
|
114
|
+
for (const [modName, entry] of cache.modules.entries()) {
|
|
115
|
+
const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
|
|
116
|
+
const moduleFileExists = currentFileHashes.has(entry.module.file);
|
|
117
|
+
const isAffectedByDep = affectedModuleNames.has(modName);
|
|
118
|
+
if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
|
|
119
|
+
modulesToKeep.set(modName, entry);
|
|
120
|
+
reusedModules.push(modName);
|
|
121
|
+
finalModules.push(entry.module);
|
|
122
|
+
if (entry.diagnostics)
|
|
123
|
+
finalDiagnostics.push(...entry.diagnostics);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const c of candidates) {
|
|
127
|
+
const modName = nameByNode.get(c.node) ?? c.className;
|
|
128
|
+
if (modulesToKeep.has(modName)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const diagBefore = ctx.diagnostics.length;
|
|
132
|
+
const parsed = parseModule(c, nameByNode, ctx);
|
|
133
|
+
const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
|
|
134
|
+
const ownedFiles = new Set;
|
|
135
|
+
ownedFiles.add(parsed.file);
|
|
136
|
+
for (const p of parsed.providers)
|
|
137
|
+
if (p.file)
|
|
138
|
+
ownedFiles.add(p.file);
|
|
139
|
+
for (const ctrl of parsed.controllers)
|
|
140
|
+
if (ctrl.file)
|
|
141
|
+
ownedFiles.add(ctrl.file);
|
|
142
|
+
const fileHashes = {};
|
|
143
|
+
for (const f of ownedFiles) {
|
|
144
|
+
fileHashes[f] = currentFileHashes.get(f) ?? "";
|
|
145
|
+
}
|
|
146
|
+
cache.modules.set(parsed.name, {
|
|
147
|
+
module: parsed,
|
|
148
|
+
ownedFiles: [...ownedFiles],
|
|
149
|
+
fileHashes,
|
|
150
|
+
diagnostics: moduleDiagnostics
|
|
151
|
+
});
|
|
152
|
+
reanalyzedModules.push(parsed.name);
|
|
153
|
+
finalModules.push(parsed);
|
|
154
|
+
finalDiagnostics.push(...moduleDiagnostics);
|
|
155
|
+
}
|
|
156
|
+
for (const modName of [...cache.modules.keys()]) {
|
|
157
|
+
if (!modulesToKeep.has(modName) && !reanalyzedModules.includes(modName)) {
|
|
158
|
+
cache.modules.delete(modName);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
cache.fileHashes = currentFileHashes;
|
|
162
|
+
cache.lastStats = { reusedModules, reanalyzedModules };
|
|
163
|
+
ctx.diagnostics = finalDiagnostics;
|
|
164
|
+
modules = finalModules;
|
|
165
|
+
} else {
|
|
166
|
+
modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
|
|
167
|
+
}
|
|
168
|
+
const allRegisteredClasses = new Set;
|
|
169
|
+
const allRegisteredControllers = new Set;
|
|
170
|
+
const allRegisteredCommands = new Set;
|
|
171
|
+
for (const m of modules) {
|
|
172
|
+
for (const p of m.providers) {
|
|
173
|
+
if (p.useClass)
|
|
174
|
+
allRegisteredClasses.add(p.useClass);
|
|
175
|
+
if (p.kind === "class")
|
|
176
|
+
allRegisteredClasses.add(p.token);
|
|
177
|
+
}
|
|
178
|
+
for (const c of m.controllers) {
|
|
179
|
+
allRegisteredControllers.add(c.className);
|
|
180
|
+
}
|
|
181
|
+
for (const cmd of m.commands) {
|
|
182
|
+
allRegisteredCommands.add(cmd.className);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const rootProviders = [];
|
|
186
|
+
const standaloneControllers = [];
|
|
187
|
+
const standaloneCommands = [];
|
|
188
|
+
for (const [name, classInfo] of ctx.classesByName.entries()) {
|
|
189
|
+
if (!allRegisteredClasses.has(name)) {
|
|
190
|
+
const injectable = parseInjectableOptions(classInfo.decl, ctx);
|
|
191
|
+
if (injectable?.providedIn === "root") {
|
|
192
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
|
|
193
|
+
const file = sourcePath(ctx.rootDir, classInfo.file);
|
|
194
|
+
const line = classInfo.decl.getStartLineNumber();
|
|
195
|
+
if (missing) {
|
|
196
|
+
warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
|
|
197
|
+
}
|
|
198
|
+
rootProviders.push({
|
|
199
|
+
token: name,
|
|
200
|
+
tokenKind: "class",
|
|
201
|
+
kind: "class",
|
|
202
|
+
useClass: name,
|
|
203
|
+
scope: injectable.scope ?? "application",
|
|
204
|
+
deps,
|
|
205
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
206
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
207
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
208
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
209
|
+
providedIn: "root",
|
|
210
|
+
hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
|
|
211
|
+
exported: true,
|
|
212
|
+
file,
|
|
213
|
+
line,
|
|
214
|
+
importPath: modulePath(ctx.rootDir, classInfo.file)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!allRegisteredControllers.has(name)) {
|
|
219
|
+
const controllerDec = findDecorator(classInfo.decl, "Controller");
|
|
220
|
+
if (controllerDec) {
|
|
221
|
+
const arg = controllerDec.getArguments()[0];
|
|
222
|
+
const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
|
|
223
|
+
if (isStandalone) {
|
|
224
|
+
const ctrl = parseController(classInfo.decl, ctx);
|
|
225
|
+
if (ctrl)
|
|
226
|
+
standaloneControllers.push(ctrl);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!allRegisteredCommands.has(name)) {
|
|
231
|
+
const commandDec = findDecorator(classInfo.decl, "Command");
|
|
232
|
+
if (commandDec) {
|
|
233
|
+
const meta = decoratorObjectArg(commandDec);
|
|
234
|
+
if (meta && booleanProp(meta, "standalone")) {
|
|
235
|
+
standaloneCommands.push({
|
|
236
|
+
className: classInfo.decl.getName() ?? name,
|
|
237
|
+
name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
|
|
238
|
+
permission: stringLiteralProp(meta, "permission"),
|
|
239
|
+
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
240
|
+
audit: stringLiteralProp(meta, "audit"),
|
|
241
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
242
|
+
standalone: true
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const [name, tokenInfo] of ctx.tokensByName.entries()) {
|
|
249
|
+
if (tokenInfo.providedIn === "root" && !allRegisteredClasses.has(name)) {
|
|
250
|
+
rootProviders.push({
|
|
251
|
+
token: name,
|
|
252
|
+
tokenKind: "injection-token",
|
|
253
|
+
kind: "factory",
|
|
254
|
+
scope: tokenInfo.scope ?? "application",
|
|
255
|
+
deps: [],
|
|
256
|
+
providedIn: "root",
|
|
257
|
+
exported: true,
|
|
258
|
+
file: sourcePath(ctx.rootDir, tokenInfo.file),
|
|
259
|
+
line: tokenInfo.line ?? 1,
|
|
260
|
+
importPath: modulePath(ctx.rootDir, tokenInfo.file)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
|
|
265
|
+
const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
|
|
266
|
+
if (existingRoot) {
|
|
267
|
+
existingRoot.providers.push(...rootProviders);
|
|
268
|
+
existingRoot.controllers.push(...standaloneControllers);
|
|
269
|
+
existingRoot.commands.push(...standaloneCommands);
|
|
270
|
+
for (const p of rootProviders) {
|
|
271
|
+
if (!existingRoot.exports.includes(p.token))
|
|
272
|
+
existingRoot.exports.push(p.token);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
|
|
276
|
+
modules.unshift({
|
|
277
|
+
name: "root",
|
|
278
|
+
className: "RootModule",
|
|
279
|
+
file: fallbackFile,
|
|
280
|
+
line: 1,
|
|
281
|
+
imports: [],
|
|
282
|
+
providers: rootProviders,
|
|
283
|
+
controllers: standaloneControllers,
|
|
284
|
+
commands: standaloneCommands,
|
|
285
|
+
queries: [],
|
|
286
|
+
exports: rootProviders.map((p) => p.token)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
79
290
|
const providedTokens = new Set(modules.flatMap((m) => m.providers.map((p) => p.token)));
|
|
80
291
|
const referenced = new Set;
|
|
81
292
|
for (const m of modules) {
|
|
@@ -94,7 +305,8 @@ async function analyzeProject(rootDir, include) {
|
|
|
94
305
|
modules,
|
|
95
306
|
externalTokens,
|
|
96
307
|
diagnostics: ctx.diagnostics,
|
|
97
|
-
tokenNames
|
|
308
|
+
tokenNames,
|
|
309
|
+
cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
|
|
98
310
|
};
|
|
99
311
|
}
|
|
100
312
|
function createProject(rootDir) {
|
|
@@ -129,7 +341,7 @@ function parseTokenVariable(decl, file) {
|
|
|
129
341
|
if (init.getExpression().getText() !== "InjectionToken")
|
|
130
342
|
return;
|
|
131
343
|
const [nameArg, optionsArg] = init.getArguments();
|
|
132
|
-
const info = { name: decl.getName(), file };
|
|
344
|
+
const info = { name: decl.getName(), file, line: decl.getStartLineNumber() };
|
|
133
345
|
if (nameArg && Node.isStringLiteral(nameArg)) {
|
|
134
346
|
info.stringName = nameArg.getLiteralText();
|
|
135
347
|
}
|
|
@@ -138,6 +350,14 @@ function parseTokenVariable(decl, file) {
|
|
|
138
350
|
if (scope && SCOPES.includes(scope)) {
|
|
139
351
|
info.scope = scope;
|
|
140
352
|
}
|
|
353
|
+
const providedIn = stringLiteralProp(optionsArg, "providedIn");
|
|
354
|
+
if (providedIn === "root") {
|
|
355
|
+
info.providedIn = "root";
|
|
356
|
+
}
|
|
357
|
+
const factory = getProp(optionsArg, "factory");
|
|
358
|
+
if (factory) {
|
|
359
|
+
info.hasFactory = true;
|
|
360
|
+
}
|
|
141
361
|
}
|
|
142
362
|
return info;
|
|
143
363
|
}
|
|
@@ -146,7 +366,8 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
146
366
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
147
367
|
const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
|
|
148
368
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
149
|
-
const
|
|
369
|
+
const unwrapped = unwrapForwardRef(el);
|
|
370
|
+
const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
|
|
150
371
|
if (decl) {
|
|
151
372
|
const known = nameByNode.get(decl);
|
|
152
373
|
if (known)
|
|
@@ -211,7 +432,8 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
211
432
|
permission: stringLiteralProp(meta, "permission"),
|
|
212
433
|
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
213
434
|
audit: stringLiteralProp(meta, "audit"),
|
|
214
|
-
idempotency: commandModeProp(meta, "idempotency") ?? "none"
|
|
435
|
+
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
436
|
+
standalone: booleanProp(meta, "standalone") || undefined
|
|
215
437
|
});
|
|
216
438
|
}
|
|
217
439
|
}
|
|
@@ -247,11 +469,13 @@ function commandModeProp(object, name) {
|
|
|
247
469
|
function parseProvider(el, exportsSet, ctx) {
|
|
248
470
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
|
|
249
471
|
const line = el.getStartLineNumber();
|
|
250
|
-
|
|
251
|
-
|
|
472
|
+
const unwrappedEl = unwrapForwardRef(el);
|
|
473
|
+
if (Node.isIdentifier(unwrappedEl)) {
|
|
474
|
+
const decl = resolveDeclaration(unwrappedEl)[0];
|
|
252
475
|
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
253
|
-
const className = cls?.getName() ??
|
|
254
|
-
const { deps, missing } = cls ? classDeps(cls, ctx) : { deps: [], missing: false };
|
|
476
|
+
const className = cls?.getName() ?? unwrappedEl.getText();
|
|
477
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], missing: false };
|
|
478
|
+
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
255
479
|
if (missing) {
|
|
256
480
|
warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
|
|
257
481
|
}
|
|
@@ -262,6 +486,12 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
262
486
|
useClass: className,
|
|
263
487
|
scope: resolveScope({ cls, tokenName: className }, ctx),
|
|
264
488
|
deps,
|
|
489
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
490
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
491
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
492
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
493
|
+
providedIn: injectable?.providedIn,
|
|
494
|
+
hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
|
|
265
495
|
exported: exportsSet.has(className),
|
|
266
496
|
file,
|
|
267
497
|
line,
|
|
@@ -276,22 +506,33 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
276
506
|
const { name: token, kind: tokenKind } = tokenNameOf(provideExpr, ctx);
|
|
277
507
|
const explicitScope = parseScopeProp(el);
|
|
278
508
|
const explicitDeps = arrayProp(el, "deps").map((d) => tokenNameOf(d, ctx).name);
|
|
509
|
+
const multi = booleanProp(el, "multi");
|
|
279
510
|
const useClassExpr = getProp(el, "useClass");
|
|
280
511
|
const useValueExpr = getProp(el, "useValue");
|
|
281
512
|
const useFactoryExpr = getProp(el, "useFactory");
|
|
282
513
|
const useExistingExpr = getProp(el, "useExisting");
|
|
283
514
|
if (useClassExpr) {
|
|
284
|
-
const
|
|
515
|
+
const unwrappedClass = unwrapForwardRef(useClassExpr);
|
|
516
|
+
const decl = Node.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass)[0] : undefined;
|
|
285
517
|
const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
|
|
286
|
-
const useClass = cls?.getName() ??
|
|
518
|
+
const useClass = cls?.getName() ?? unwrappedClass.getText();
|
|
287
519
|
let deps = explicitDeps;
|
|
520
|
+
let optionalDeps = [];
|
|
521
|
+
let selfDeps = [];
|
|
522
|
+
let skipSelfDeps = [];
|
|
523
|
+
let hostDeps = [];
|
|
288
524
|
if (deps.length === 0 && cls) {
|
|
289
525
|
const result = classDeps(cls, ctx);
|
|
290
526
|
deps = result.deps;
|
|
527
|
+
optionalDeps = result.optionalDeps;
|
|
528
|
+
selfDeps = result.selfDeps;
|
|
529
|
+
skipSelfDeps = result.skipSelfDeps;
|
|
530
|
+
hostDeps = result.hostDeps;
|
|
291
531
|
if (result.missing) {
|
|
292
532
|
warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
|
|
293
533
|
}
|
|
294
534
|
}
|
|
535
|
+
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
295
536
|
return {
|
|
296
537
|
token,
|
|
297
538
|
tokenKind,
|
|
@@ -299,6 +540,13 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
299
540
|
useClass,
|
|
300
541
|
scope: resolveScope({ explicit: explicitScope, cls, tokenName: token }, ctx),
|
|
301
542
|
deps,
|
|
543
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
544
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
545
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
546
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
547
|
+
multi: multi ?? undefined,
|
|
548
|
+
providedIn: injectable?.providedIn,
|
|
549
|
+
hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
|
|
302
550
|
exported: exportsSet.has(token),
|
|
303
551
|
file,
|
|
304
552
|
line,
|
|
@@ -313,6 +561,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
313
561
|
useValueExpr: useValueExpr.getText(),
|
|
314
562
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
315
563
|
deps: [],
|
|
564
|
+
multi: multi ?? undefined,
|
|
316
565
|
exported: exportsSet.has(token),
|
|
317
566
|
file,
|
|
318
567
|
line,
|
|
@@ -331,6 +580,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
331
580
|
useFactoryName: factoryName,
|
|
332
581
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
333
582
|
deps: explicitDeps,
|
|
583
|
+
multi: multi ?? undefined,
|
|
334
584
|
exported: exportsSet.has(token),
|
|
335
585
|
file,
|
|
336
586
|
line,
|
|
@@ -346,6 +596,7 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
346
596
|
useExisting: target,
|
|
347
597
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
348
598
|
deps: [target],
|
|
599
|
+
multi: multi ?? undefined,
|
|
349
600
|
exported: exportsSet.has(token),
|
|
350
601
|
file,
|
|
351
602
|
line
|
|
@@ -353,18 +604,36 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
353
604
|
}
|
|
354
605
|
return;
|
|
355
606
|
}
|
|
356
|
-
function parseController(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
607
|
+
function parseController(input, ctx) {
|
|
608
|
+
let decl;
|
|
609
|
+
if (Node.isClassDeclaration(input)) {
|
|
610
|
+
decl = input;
|
|
611
|
+
} else {
|
|
612
|
+
const unwrapped = unwrapForwardRef(input);
|
|
613
|
+
const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
|
|
614
|
+
if (resolved && Node.isClassDeclaration(resolved)) {
|
|
615
|
+
decl = resolved;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (!decl)
|
|
361
619
|
return;
|
|
362
620
|
const controllerDec = findDecorator(decl, "Controller");
|
|
363
621
|
if (!controllerDec)
|
|
364
622
|
return;
|
|
623
|
+
let path = "/";
|
|
624
|
+
let standalone;
|
|
365
625
|
const pathArg = controllerDec.getArguments()[0];
|
|
366
|
-
|
|
367
|
-
|
|
626
|
+
if (pathArg) {
|
|
627
|
+
if (Node.isStringLiteral(pathArg)) {
|
|
628
|
+
path = pathArg.getLiteralText();
|
|
629
|
+
} else if (Node.isObjectLiteralExpression(pathArg)) {
|
|
630
|
+
const p = stringLiteralProp(pathArg, "path");
|
|
631
|
+
if (p)
|
|
632
|
+
path = p;
|
|
633
|
+
standalone = booleanProp(pathArg, "standalone");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
|
|
368
637
|
const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
|
|
369
638
|
if (missing) {
|
|
370
639
|
warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
|
|
@@ -372,6 +641,14 @@ function parseController(el, ctx) {
|
|
|
372
641
|
const injectable = parseInjectableOptions(decl, ctx);
|
|
373
642
|
const routes = [];
|
|
374
643
|
const schemaImports = {};
|
|
644
|
+
const classGuards = [];
|
|
645
|
+
for (const dec of decl.getDecorators()) {
|
|
646
|
+
if (decoratorName(dec) === "UseGuards") {
|
|
647
|
+
for (const gArg of dec.getArguments()) {
|
|
648
|
+
classGuards.push(tokenText(gArg));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
375
652
|
for (const method of decl.getMethods()) {
|
|
376
653
|
for (const dec of method.getDecorators()) {
|
|
377
654
|
const name = decoratorName(dec);
|
|
@@ -380,11 +657,158 @@ function parseController(el, ctx) {
|
|
|
380
657
|
continue;
|
|
381
658
|
const args = dec.getArguments();
|
|
382
659
|
const pathArg2 = args[0];
|
|
660
|
+
const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
|
|
383
661
|
const route = {
|
|
384
662
|
method: httpMethod,
|
|
385
|
-
path:
|
|
663
|
+
path: routePath,
|
|
386
664
|
handler: method.getName()
|
|
387
665
|
};
|
|
666
|
+
const pathParams = [];
|
|
667
|
+
const paramRegex = /:([a-zA-Z0-9_]+)/g;
|
|
668
|
+
let match;
|
|
669
|
+
while ((match = paramRegex.exec(routePath)) !== null) {
|
|
670
|
+
pathParams.push(match[1]);
|
|
671
|
+
}
|
|
672
|
+
if (pathParams.length > 0)
|
|
673
|
+
route.pathParams = pathParams;
|
|
674
|
+
const paramBindings = [];
|
|
675
|
+
const queryBindings = [];
|
|
676
|
+
const paramTransforms = {};
|
|
677
|
+
const paramDefaults = {};
|
|
678
|
+
const queryTransforms = {};
|
|
679
|
+
const queryDefaults = {};
|
|
680
|
+
let hasBodyBinding = false;
|
|
681
|
+
const handlerParams = [];
|
|
682
|
+
for (const p of method.getParameters()) {
|
|
683
|
+
const pName = p.getName();
|
|
684
|
+
let hasBindingDecorator = false;
|
|
685
|
+
let paramNode;
|
|
686
|
+
for (const pDec of p.getDecorators()) {
|
|
687
|
+
const dName = decoratorName(pDec);
|
|
688
|
+
const dArgs = pDec.getArguments();
|
|
689
|
+
if (dName === "Param") {
|
|
690
|
+
hasBindingDecorator = true;
|
|
691
|
+
const parsed = parseBindingOptions(dArgs, pName);
|
|
692
|
+
paramBindings.push(parsed.name);
|
|
693
|
+
if (parsed.transform)
|
|
694
|
+
paramTransforms[parsed.name] = parsed.transform;
|
|
695
|
+
if (parsed.default !== undefined)
|
|
696
|
+
paramDefaults[parsed.name] = parsed.default;
|
|
697
|
+
paramNode = {
|
|
698
|
+
name: pName,
|
|
699
|
+
kind: "param",
|
|
700
|
+
bindingName: parsed.name,
|
|
701
|
+
transform: parsed.transform,
|
|
702
|
+
default: parsed.default
|
|
703
|
+
};
|
|
704
|
+
} else if (dName === "Query") {
|
|
705
|
+
hasBindingDecorator = true;
|
|
706
|
+
const parsed = parseBindingOptions(dArgs, pName);
|
|
707
|
+
queryBindings.push(parsed.name);
|
|
708
|
+
if (parsed.transform)
|
|
709
|
+
queryTransforms[parsed.name] = parsed.transform;
|
|
710
|
+
if (parsed.default !== undefined)
|
|
711
|
+
queryDefaults[parsed.name] = parsed.default;
|
|
712
|
+
paramNode = {
|
|
713
|
+
name: pName,
|
|
714
|
+
kind: "query",
|
|
715
|
+
bindingName: parsed.name,
|
|
716
|
+
transform: parsed.transform,
|
|
717
|
+
default: parsed.default
|
|
718
|
+
};
|
|
719
|
+
} else if (dName === "Body") {
|
|
720
|
+
hasBindingDecorator = true;
|
|
721
|
+
hasBodyBinding = true;
|
|
722
|
+
paramNode = { name: pName, kind: "body" };
|
|
723
|
+
} else if (dName === "Headers") {
|
|
724
|
+
hasBindingDecorator = true;
|
|
725
|
+
paramNode = { name: pName, kind: "headers" };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!hasBindingDecorator && pathParams.includes(pName)) {
|
|
729
|
+
paramBindings.push(pName);
|
|
730
|
+
const typeText = p.getType().getText();
|
|
731
|
+
let inferredTransform;
|
|
732
|
+
if (typeText === "number") {
|
|
733
|
+
paramTransforms[pName] = "number";
|
|
734
|
+
inferredTransform = "number";
|
|
735
|
+
} else if (typeText === "boolean") {
|
|
736
|
+
paramTransforms[pName] = "boolean";
|
|
737
|
+
inferredTransform = "boolean";
|
|
738
|
+
}
|
|
739
|
+
paramNode = {
|
|
740
|
+
name: pName,
|
|
741
|
+
kind: "param",
|
|
742
|
+
bindingName: pName,
|
|
743
|
+
transform: inferredTransform
|
|
744
|
+
};
|
|
745
|
+
} else if (!hasBindingDecorator) {
|
|
746
|
+
if (pName === "req" || pName === "ctx" || pName === "context") {
|
|
747
|
+
paramNode = { name: pName, kind: "context" };
|
|
748
|
+
} else {
|
|
749
|
+
paramNode = { name: pName, kind: "unknown" };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (paramNode)
|
|
753
|
+
handlerParams.push(paramNode);
|
|
754
|
+
}
|
|
755
|
+
if (paramBindings.length > 0)
|
|
756
|
+
route.paramBindings = paramBindings;
|
|
757
|
+
if (queryBindings.length > 0)
|
|
758
|
+
route.queryBindings = queryBindings;
|
|
759
|
+
if (Object.keys(paramTransforms).length > 0)
|
|
760
|
+
route.paramTransforms = paramTransforms;
|
|
761
|
+
if (Object.keys(paramDefaults).length > 0)
|
|
762
|
+
route.paramDefaults = paramDefaults;
|
|
763
|
+
if (Object.keys(queryTransforms).length > 0)
|
|
764
|
+
route.queryTransforms = queryTransforms;
|
|
765
|
+
if (Object.keys(queryDefaults).length > 0)
|
|
766
|
+
route.queryDefaults = queryDefaults;
|
|
767
|
+
if (hasBodyBinding)
|
|
768
|
+
route.hasBodyBinding = true;
|
|
769
|
+
if (handlerParams.length > 0)
|
|
770
|
+
route.handlerParams = handlerParams;
|
|
771
|
+
const routeGuards = [...classGuards];
|
|
772
|
+
const routeCanDeactivate = [];
|
|
773
|
+
for (const mDec of method.getDecorators()) {
|
|
774
|
+
const dName = decoratorName(mDec);
|
|
775
|
+
const mArgs = mDec.getArguments();
|
|
776
|
+
if (dName === "UseGuards") {
|
|
777
|
+
for (const gArg of mArgs) {
|
|
778
|
+
routeGuards.push(tokenText(gArg));
|
|
779
|
+
}
|
|
780
|
+
} else if (dName === "CanDeactivate") {
|
|
781
|
+
for (const gArg of mArgs) {
|
|
782
|
+
routeCanDeactivate.push(tokenText(gArg));
|
|
783
|
+
}
|
|
784
|
+
} else if (dName === "Title") {
|
|
785
|
+
const tArg = mArgs[0];
|
|
786
|
+
if (tArg && Node.isStringLiteral(tArg)) {
|
|
787
|
+
route.title = tArg.getLiteralText();
|
|
788
|
+
}
|
|
789
|
+
} else if (dName === "Data") {
|
|
790
|
+
const dArg = mArgs[0];
|
|
791
|
+
if (dArg && Node.isObjectLiteralExpression(dArg)) {
|
|
792
|
+
route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
|
|
793
|
+
}
|
|
794
|
+
} else if (dName === "Resolve") {
|
|
795
|
+
const rArg = mArgs[0];
|
|
796
|
+
if (rArg && Node.isObjectLiteralExpression(rArg)) {
|
|
797
|
+
const resolvers = route.resolvers ?? {};
|
|
798
|
+
for (const prop of rArg.getProperties()) {
|
|
799
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
800
|
+
const rName = prop.getName();
|
|
801
|
+
const init = prop.getInitializer();
|
|
802
|
+
if (init)
|
|
803
|
+
resolvers[rName] = tokenText(init);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (Object.keys(resolvers).length > 0) {
|
|
807
|
+
route.resolvers = resolvers;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
388
812
|
const optionsArg = args[1];
|
|
389
813
|
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
390
814
|
for (const field of ["body", "params", "query", "response"]) {
|
|
@@ -401,6 +825,68 @@ function parseController(el, ctx) {
|
|
|
401
825
|
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
402
826
|
route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
|
|
403
827
|
}
|
|
828
|
+
const guardsExpr = getProp(optionsArg, "guards");
|
|
829
|
+
if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
|
|
830
|
+
for (const el of guardsExpr.getElements()) {
|
|
831
|
+
routeGuards.push(tokenText(el));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const canMatchExpr = getProp(optionsArg, "canMatch");
|
|
835
|
+
if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
|
|
836
|
+
const canMatchList = [];
|
|
837
|
+
for (const el of canMatchExpr.getElements()) {
|
|
838
|
+
canMatchList.push(tokenText(el));
|
|
839
|
+
}
|
|
840
|
+
if (canMatchList.length > 0) {
|
|
841
|
+
route.canMatch = canMatchList;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
|
|
845
|
+
if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
|
|
846
|
+
for (const el of canDeactivateExpr.getElements()) {
|
|
847
|
+
routeCanDeactivate.push(tokenText(el));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const resolversExpr = getProp(optionsArg, "resolvers");
|
|
851
|
+
if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
|
|
852
|
+
const resolvers = {};
|
|
853
|
+
for (const prop of resolversExpr.getProperties()) {
|
|
854
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
855
|
+
const rName = prop.getName();
|
|
856
|
+
const init = prop.getInitializer();
|
|
857
|
+
if (init)
|
|
858
|
+
resolvers[rName] = tokenText(init);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (Object.keys(resolvers).length > 0) {
|
|
862
|
+
route.resolvers = resolvers;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const redirectToExpr = getProp(optionsArg, "redirectTo");
|
|
866
|
+
if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
|
|
867
|
+
route.redirectTo = redirectToExpr.getLiteralText();
|
|
868
|
+
}
|
|
869
|
+
const pathMatchExpr = getProp(optionsArg, "pathMatch");
|
|
870
|
+
if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
|
|
871
|
+
const val = pathMatchExpr.getLiteralText();
|
|
872
|
+
if (val === "full" || val === "prefix") {
|
|
873
|
+
route.pathMatch = val;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const titleExpr = getProp(optionsArg, "title");
|
|
877
|
+
if (titleExpr && Node.isStringLiteral(titleExpr)) {
|
|
878
|
+
route.title = titleExpr.getLiteralText();
|
|
879
|
+
}
|
|
880
|
+
const dataExpr = getProp(optionsArg, "data");
|
|
881
|
+
if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
|
|
882
|
+
route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (routeGuards.length > 0) {
|
|
886
|
+
route.guards = routeGuards;
|
|
887
|
+
}
|
|
888
|
+
if (routeCanDeactivate.length > 0) {
|
|
889
|
+
route.canDeactivate = routeCanDeactivate;
|
|
404
890
|
}
|
|
405
891
|
routes.push(route);
|
|
406
892
|
}
|
|
@@ -410,6 +896,10 @@ function parseController(el, ctx) {
|
|
|
410
896
|
path,
|
|
411
897
|
scope: injectable?.scope ?? "request",
|
|
412
898
|
deps,
|
|
899
|
+
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
900
|
+
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
901
|
+
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
902
|
+
standalone: standalone || undefined,
|
|
413
903
|
routes,
|
|
414
904
|
file,
|
|
415
905
|
importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
|
|
@@ -418,28 +908,84 @@ function parseController(el, ctx) {
|
|
|
418
908
|
}
|
|
419
909
|
function classDeps(cls, ctx) {
|
|
420
910
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
421
|
-
if (injectable?.deps)
|
|
422
|
-
return {
|
|
911
|
+
if (injectable?.deps) {
|
|
912
|
+
return {
|
|
913
|
+
deps: injectable.deps,
|
|
914
|
+
optionalDeps: [],
|
|
915
|
+
selfDeps: [],
|
|
916
|
+
skipSelfDeps: [],
|
|
917
|
+
hostDeps: [],
|
|
918
|
+
missing: false
|
|
919
|
+
};
|
|
920
|
+
}
|
|
423
921
|
const ctor = cls.getConstructors()[0];
|
|
424
|
-
if (!ctor || ctor.getParameters().length === 0)
|
|
425
|
-
return { deps: [], missing: false };
|
|
426
|
-
const injectParams = parseInjectParams(cls);
|
|
427
922
|
const deps = [];
|
|
923
|
+
const optionalDeps = [];
|
|
924
|
+
const selfDeps = [];
|
|
925
|
+
const skipSelfDeps = [];
|
|
926
|
+
const hostDeps = [];
|
|
428
927
|
let missing = false;
|
|
429
|
-
ctor.getParameters().
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
928
|
+
if (ctor && ctor.getParameters().length > 0) {
|
|
929
|
+
const injectParams = parseInjectParams(cls);
|
|
930
|
+
const optionalIndices = parseOptionalParams(cls);
|
|
931
|
+
const selfIndices = parseModifierParams(cls, "Self");
|
|
932
|
+
const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
|
|
933
|
+
const hostIndices = parseModifierParams(cls, "Host");
|
|
934
|
+
ctor.getParameters().forEach((param, index) => {
|
|
935
|
+
const isOptional = optionalIndices.has(index);
|
|
936
|
+
const injected = injectParams.get(index);
|
|
937
|
+
const tokenName = injected ?? paramTypeTokenName(param, ctx);
|
|
938
|
+
if (tokenName) {
|
|
939
|
+
deps.push(tokenName);
|
|
940
|
+
if (isOptional)
|
|
941
|
+
optionalDeps.push(tokenName);
|
|
942
|
+
if (selfIndices.has(index))
|
|
943
|
+
selfDeps.push(tokenName);
|
|
944
|
+
if (skipSelfIndices.has(index))
|
|
945
|
+
skipSelfDeps.push(tokenName);
|
|
946
|
+
if (hostIndices.has(index))
|
|
947
|
+
hostDeps.push(tokenName);
|
|
948
|
+
} else {
|
|
949
|
+
if (!isOptional)
|
|
950
|
+
missing = true;
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
for (const prop of cls.getProperties()) {
|
|
955
|
+
const init = prop.getInitializer();
|
|
956
|
+
if (init && Node.isCallExpression(init)) {
|
|
957
|
+
const callName = init.getExpression().getText().split(".").pop();
|
|
958
|
+
if (callName === "inject") {
|
|
959
|
+
const [tokenArg, optionsArg] = init.getArguments();
|
|
960
|
+
if (tokenArg) {
|
|
961
|
+
const tokenName = tokenText(tokenArg);
|
|
962
|
+
if (tokenName) {
|
|
963
|
+
if (!deps.includes(tokenName))
|
|
964
|
+
deps.push(tokenName);
|
|
965
|
+
if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
|
|
966
|
+
const isOptional = booleanProp(optionsArg, "optional");
|
|
967
|
+
if (isOptional && !optionalDeps.includes(tokenName)) {
|
|
968
|
+
optionalDeps.push(tokenName);
|
|
969
|
+
}
|
|
970
|
+
const isSelf = booleanProp(optionsArg, "self");
|
|
971
|
+
if (isSelf && !selfDeps.includes(tokenName)) {
|
|
972
|
+
selfDeps.push(tokenName);
|
|
973
|
+
}
|
|
974
|
+
const isSkipSelf = booleanProp(optionsArg, "skipSelf");
|
|
975
|
+
if (isSkipSelf && !skipSelfDeps.includes(tokenName)) {
|
|
976
|
+
skipSelfDeps.push(tokenName);
|
|
977
|
+
}
|
|
978
|
+
const isHost = booleanProp(optionsArg, "host");
|
|
979
|
+
if (isHost && !hostDeps.includes(tokenName)) {
|
|
980
|
+
hostDeps.push(tokenName);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
440
986
|
}
|
|
441
|
-
}
|
|
442
|
-
return { deps, missing };
|
|
987
|
+
}
|
|
988
|
+
return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
|
|
443
989
|
}
|
|
444
990
|
function paramTypeTokenName(param, ctx) {
|
|
445
991
|
const typeNode = param.getTypeNode();
|
|
@@ -460,9 +1006,11 @@ function parseInjectableOptions(cls, ctx) {
|
|
|
460
1006
|
if (!obj)
|
|
461
1007
|
return {};
|
|
462
1008
|
const scope = stringLiteralProp(obj, "scope");
|
|
1009
|
+
const providedIn = stringLiteralProp(obj, "providedIn");
|
|
463
1010
|
const depsExpr = getProp(obj, "deps");
|
|
464
1011
|
return {
|
|
465
1012
|
scope: scope && SCOPES.includes(scope) ? scope : undefined,
|
|
1013
|
+
providedIn: providedIn === "root" ? "root" : undefined,
|
|
466
1014
|
deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
|
|
467
1015
|
};
|
|
468
1016
|
}
|
|
@@ -482,15 +1030,61 @@ function parseInjectParams(cls) {
|
|
|
482
1030
|
});
|
|
483
1031
|
return result;
|
|
484
1032
|
}
|
|
1033
|
+
function parseOptionalParams(cls) {
|
|
1034
|
+
const result = new Set;
|
|
1035
|
+
const ctor = cls.getConstructors()[0];
|
|
1036
|
+
if (!ctor)
|
|
1037
|
+
return result;
|
|
1038
|
+
ctor.getParameters().forEach((param, index) => {
|
|
1039
|
+
for (const dec of param.getDecorators()) {
|
|
1040
|
+
if (decoratorName(dec) === "Optional")
|
|
1041
|
+
result.add(index);
|
|
1042
|
+
}
|
|
1043
|
+
if (param.hasQuestionToken())
|
|
1044
|
+
result.add(index);
|
|
1045
|
+
});
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
function parseModifierParams(cls, modifierName) {
|
|
1049
|
+
const result = new Set;
|
|
1050
|
+
const ctor = cls.getConstructors()[0];
|
|
1051
|
+
if (!ctor)
|
|
1052
|
+
return result;
|
|
1053
|
+
ctor.getParameters().forEach((param, index) => {
|
|
1054
|
+
for (const dec of param.getDecorators()) {
|
|
1055
|
+
if (decoratorName(dec) === modifierName)
|
|
1056
|
+
result.add(index);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
function unwrapForwardRef(expr) {
|
|
1062
|
+
if (Node.isCallExpression(expr)) {
|
|
1063
|
+
const exprText = expr.getExpression().getText();
|
|
1064
|
+
if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
|
|
1065
|
+
const arg = expr.getArguments()[0];
|
|
1066
|
+
if (arg && (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg))) {
|
|
1067
|
+
const body = arg.getBody();
|
|
1068
|
+
if (body && Node.isExpression(body)) {
|
|
1069
|
+
return unwrapForwardRef(body);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return expr;
|
|
1075
|
+
}
|
|
485
1076
|
function tokenText(expr) {
|
|
486
|
-
|
|
487
|
-
|
|
1077
|
+
const unwrapped = unwrapForwardRef(expr);
|
|
1078
|
+
if (Node.isStringLiteral(unwrapped))
|
|
1079
|
+
return unwrapped.getLiteralText();
|
|
1080
|
+
if (Node.isIdentifier(unwrapped)) {
|
|
1081
|
+
const decl = resolveDeclaration(unwrapped)[0];
|
|
488
1082
|
if (decl && Node.isClassDeclaration(decl))
|
|
489
|
-
return decl.getName() ??
|
|
1083
|
+
return decl.getName() ?? unwrapped.getText();
|
|
490
1084
|
if (decl && Node.isVariableDeclaration(decl))
|
|
491
1085
|
return decl.getName();
|
|
492
1086
|
}
|
|
493
|
-
return
|
|
1087
|
+
return unwrapped.getText();
|
|
494
1088
|
}
|
|
495
1089
|
function resolveScope(input, ctx) {
|
|
496
1090
|
if (input.explicit)
|
|
@@ -506,8 +1100,9 @@ function resolveScope(input, ctx) {
|
|
|
506
1100
|
return "application";
|
|
507
1101
|
}
|
|
508
1102
|
function tokenNameOf(expr, ctx) {
|
|
509
|
-
|
|
510
|
-
|
|
1103
|
+
const unwrapped = unwrapForwardRef(expr);
|
|
1104
|
+
if (Node.isIdentifier(unwrapped)) {
|
|
1105
|
+
const decl = resolveDeclaration(unwrapped)[0];
|
|
511
1106
|
if (decl && Node.isClassDeclaration(decl)) {
|
|
512
1107
|
return { name: decl.getName() ?? expr.getText(), kind: "class" };
|
|
513
1108
|
}
|
|
@@ -585,10 +1180,90 @@ function arrayProp(obj, name) {
|
|
|
585
1180
|
const expr = getProp(obj, name);
|
|
586
1181
|
return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
|
|
587
1182
|
}
|
|
1183
|
+
function booleanProp(obj, name) {
|
|
1184
|
+
const expr = getProp(obj, name);
|
|
1185
|
+
if (!expr)
|
|
1186
|
+
return;
|
|
1187
|
+
if (expr.getKind() === SyntaxKind.TrueKeyword)
|
|
1188
|
+
return true;
|
|
1189
|
+
if (expr.getKind() === SyntaxKind.FalseKeyword)
|
|
1190
|
+
return false;
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
588
1193
|
function parseScopeProp(obj) {
|
|
589
1194
|
const scope = stringLiteralProp(obj, "scope");
|
|
590
1195
|
return scope && SCOPES.includes(scope) ? scope : undefined;
|
|
591
1196
|
}
|
|
1197
|
+
function parseBindingOptions(args, defaultName) {
|
|
1198
|
+
let name = defaultName;
|
|
1199
|
+
let transform;
|
|
1200
|
+
let defaultValue;
|
|
1201
|
+
const first = args[0];
|
|
1202
|
+
const second = args[1];
|
|
1203
|
+
if (first && Node.isStringLiteral(first)) {
|
|
1204
|
+
name = first.getLiteralText();
|
|
1205
|
+
} else if (first && Node.isObjectLiteralExpression(first)) {
|
|
1206
|
+
const nameProp = getProp(first, "name");
|
|
1207
|
+
if (nameProp && Node.isStringLiteral(nameProp)) {
|
|
1208
|
+
name = nameProp.getLiteralText();
|
|
1209
|
+
}
|
|
1210
|
+
const trProp = getProp(first, "transform");
|
|
1211
|
+
if (trProp && Node.isStringLiteral(trProp)) {
|
|
1212
|
+
const val = trProp.getLiteralText();
|
|
1213
|
+
if (val === "number" || val === "boolean" || val === "string") {
|
|
1214
|
+
transform = val;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const defProp = getProp(first, "default");
|
|
1218
|
+
if (defProp) {
|
|
1219
|
+
defaultValue = parseLiteralValue(defProp);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (second && Node.isObjectLiteralExpression(second)) {
|
|
1223
|
+
const trProp = getProp(second, "transform");
|
|
1224
|
+
if (trProp && Node.isStringLiteral(trProp)) {
|
|
1225
|
+
const val = trProp.getLiteralText();
|
|
1226
|
+
if (val === "number" || val === "boolean" || val === "string") {
|
|
1227
|
+
transform = val;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const defProp = getProp(second, "default");
|
|
1231
|
+
if (defProp) {
|
|
1232
|
+
defaultValue = parseLiteralValue(defProp);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return { name, transform, default: defaultValue };
|
|
1236
|
+
}
|
|
1237
|
+
function parseLiteralValue(node) {
|
|
1238
|
+
if (Node.isStringLiteral(node))
|
|
1239
|
+
return node.getLiteralText();
|
|
1240
|
+
if (Node.isNumericLiteral(node))
|
|
1241
|
+
return node.getLiteralValue();
|
|
1242
|
+
if (node.getKindName() === "TrueKeyword")
|
|
1243
|
+
return true;
|
|
1244
|
+
if (node.getKindName() === "FalseKeyword")
|
|
1245
|
+
return false;
|
|
1246
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
1247
|
+
return node.getElements().map(parseLiteralValue);
|
|
1248
|
+
}
|
|
1249
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
1250
|
+
return parseObjectLiteralValues(node);
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
function parseObjectLiteralValues(obj) {
|
|
1255
|
+
const result = {};
|
|
1256
|
+
for (const prop of obj.getProperties()) {
|
|
1257
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
1258
|
+
const name = prop.getName();
|
|
1259
|
+
const init = prop.getInitializer();
|
|
1260
|
+
if (init) {
|
|
1261
|
+
result[name] = parseLiteralValue(init);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
}
|
|
592
1267
|
function modulePath(rootDir, absFile) {
|
|
593
1268
|
return sourcePath(rootDir, absFile).replace(/\.(ts|tsx|js|mts|cts)$/, "");
|
|
594
1269
|
}
|
|
@@ -600,7 +1275,7 @@ function warn(ctx, code, message, file, line) {
|
|
|
600
1275
|
}
|
|
601
1276
|
|
|
602
1277
|
// src/generate.ts
|
|
603
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1278
|
+
import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
604
1279
|
import { join as join2 } from "node:path";
|
|
605
1280
|
|
|
606
1281
|
// src/util.ts
|
|
@@ -633,6 +1308,28 @@ function isRequestContextToken(token, tokenNames) {
|
|
|
633
1308
|
function isJobContextToken(token, tokenNames) {
|
|
634
1309
|
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
635
1310
|
}
|
|
1311
|
+
function joinRoutePaths(prefix, path) {
|
|
1312
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1313
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1314
|
+
return normalized;
|
|
1315
|
+
}
|
|
1316
|
+
function findClosestMatch(target, candidates) {
|
|
1317
|
+
if (candidates.length === 0)
|
|
1318
|
+
return;
|
|
1319
|
+
if (candidates.length === 1)
|
|
1320
|
+
return candidates[0];
|
|
1321
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1322
|
+
const targetNorm = norm(target);
|
|
1323
|
+
for (const c of candidates) {
|
|
1324
|
+
if (norm(c) === targetNorm)
|
|
1325
|
+
return c;
|
|
1326
|
+
}
|
|
1327
|
+
for (const c of candidates) {
|
|
1328
|
+
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
1329
|
+
return c;
|
|
1330
|
+
}
|
|
1331
|
+
return candidates[0];
|
|
1332
|
+
}
|
|
636
1333
|
|
|
637
1334
|
// src/generate.ts
|
|
638
1335
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
@@ -645,6 +1342,28 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
645
1342
|
query?: unknown;
|
|
646
1343
|
response?: unknown;
|
|
647
1344
|
command?: string;
|
|
1345
|
+
guards?: string[];
|
|
1346
|
+
canMatch?: string[];
|
|
1347
|
+
canDeactivate?: string[];
|
|
1348
|
+
resolvers?: Record<string, string>;
|
|
1349
|
+
redirectTo?: string;
|
|
1350
|
+
pathMatch?: "full" | "prefix";
|
|
1351
|
+
paramTransforms?: Record<string, "number" | "boolean" | "string">;
|
|
1352
|
+
paramDefaults?: Record<string, unknown>;
|
|
1353
|
+
queryTransforms?: Record<string, "number" | "boolean" | "string">;
|
|
1354
|
+
queryDefaults?: Record<string, unknown>;
|
|
1355
|
+
title?: string;
|
|
1356
|
+
data?: Record<string, unknown>;
|
|
1357
|
+
invoker?: (
|
|
1358
|
+
controller: unknown,
|
|
1359
|
+
request: {
|
|
1360
|
+
params?: Record<string, unknown>;
|
|
1361
|
+
query?: Record<string, unknown>;
|
|
1362
|
+
body?: unknown;
|
|
1363
|
+
headers?: Record<string, unknown>;
|
|
1364
|
+
context?: unknown;
|
|
1365
|
+
},
|
|
1366
|
+
) => Promise<unknown> | unknown;
|
|
648
1367
|
}
|
|
649
1368
|
|
|
650
1369
|
export interface CompiledCommand {
|
|
@@ -654,6 +1373,7 @@ export interface CompiledCommand {
|
|
|
654
1373
|
transaction: "required" | "none";
|
|
655
1374
|
audit?: string;
|
|
656
1375
|
idempotency: "required" | "none";
|
|
1376
|
+
standalone?: boolean;
|
|
657
1377
|
}
|
|
658
1378
|
|
|
659
1379
|
export interface CompiledController {
|
|
@@ -683,7 +1403,40 @@ export interface CompiledModule {
|
|
|
683
1403
|
commands: CompiledCommand[];
|
|
684
1404
|
}`;
|
|
685
1405
|
function renderApplication(graph, options) {
|
|
686
|
-
|
|
1406
|
+
let modules = topoSortModules(graph.modules);
|
|
1407
|
+
if (options.treeShakeUnusedProviders) {
|
|
1408
|
+
const referencedTokens = new Set;
|
|
1409
|
+
for (const mod of graph.modules) {
|
|
1410
|
+
for (const exp of mod.exports)
|
|
1411
|
+
referencedTokens.add(exp);
|
|
1412
|
+
for (const ctrl of mod.controllers) {
|
|
1413
|
+
for (const d of ctrl.deps)
|
|
1414
|
+
referencedTokens.add(d);
|
|
1415
|
+
for (const d of ctrl.optionalDeps ?? [])
|
|
1416
|
+
referencedTokens.add(d);
|
|
1417
|
+
for (const d of ctrl.selfDeps ?? [])
|
|
1418
|
+
referencedTokens.add(d);
|
|
1419
|
+
for (const d of ctrl.skipSelfDeps ?? [])
|
|
1420
|
+
referencedTokens.add(d);
|
|
1421
|
+
}
|
|
1422
|
+
for (const p of mod.providers) {
|
|
1423
|
+
for (const d of p.deps ?? [])
|
|
1424
|
+
referencedTokens.add(d);
|
|
1425
|
+
for (const d of p.optionalDeps ?? [])
|
|
1426
|
+
referencedTokens.add(d);
|
|
1427
|
+
for (const d of p.selfDeps ?? [])
|
|
1428
|
+
referencedTokens.add(d);
|
|
1429
|
+
for (const d of p.skipSelfDeps ?? [])
|
|
1430
|
+
referencedTokens.add(d);
|
|
1431
|
+
if (p.useExisting)
|
|
1432
|
+
referencedTokens.add(p.useExisting);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
modules = modules.map((mod) => ({
|
|
1436
|
+
...mod,
|
|
1437
|
+
providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
|
|
1438
|
+
}));
|
|
1439
|
+
}
|
|
687
1440
|
const imports = new ImportManager;
|
|
688
1441
|
const factorySections = [];
|
|
689
1442
|
const descriptorEntries = [];
|
|
@@ -705,6 +1458,34 @@ function renderApplication(graph, options) {
|
|
|
705
1458
|
" ];",
|
|
706
1459
|
"}",
|
|
707
1460
|
"",
|
|
1461
|
+
"export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1462
|
+
' const initializers = (services.appInitializer ?? (services as any)["supacloud.app-initializer"]) as unknown;',
|
|
1463
|
+
" if (Array.isArray(initializers)) {",
|
|
1464
|
+
" for (const init of initializers) {",
|
|
1465
|
+
' if (typeof init === "function") await init();',
|
|
1466
|
+
" }",
|
|
1467
|
+
' } else if (typeof initializers === "function") {',
|
|
1468
|
+
" await (initializers as () => unknown)();",
|
|
1469
|
+
" }",
|
|
1470
|
+
"}",
|
|
1471
|
+
"",
|
|
1472
|
+
"export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1473
|
+
' const destroyRef = (services.destroyRef ?? (services as any)["supacloud.destroy-ref"]) as { destroy?: () => Promise<void>; _teardowns?: Array<() => void | Promise<void>> } | undefined;',
|
|
1474
|
+
' if (destroyRef && typeof destroyRef.destroy === "function") {',
|
|
1475
|
+
" await destroyRef.destroy();",
|
|
1476
|
+
" } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
|
|
1477
|
+
" for (const teardown of [...destroyRef._teardowns].reverse()) {",
|
|
1478
|
+
' if (typeof teardown === "function") await teardown();',
|
|
1479
|
+
" }",
|
|
1480
|
+
" }",
|
|
1481
|
+
" const instances = Object.values(services);",
|
|
1482
|
+
" for (const inst of instances.reverse()) {",
|
|
1483
|
+
' if (inst && typeof (inst as any).onDestroy === "function") {',
|
|
1484
|
+
" await (inst as any).onDestroy();",
|
|
1485
|
+
" }",
|
|
1486
|
+
" }",
|
|
1487
|
+
"}",
|
|
1488
|
+
"",
|
|
708
1489
|
...factorySections,
|
|
709
1490
|
""
|
|
710
1491
|
].join(`
|
|
@@ -714,10 +1495,14 @@ function renderApplication(graph, options) {
|
|
|
714
1495
|
modules: graph.modules,
|
|
715
1496
|
externalTokens: graph.externalTokens
|
|
716
1497
|
};
|
|
1498
|
+
const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
|
|
1499
|
+
const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
|
|
717
1500
|
return {
|
|
718
1501
|
applicationCode: code,
|
|
719
1502
|
manifestJson: JSON.stringify(manifest, null, 2) + `
|
|
720
|
-
|
|
1503
|
+
`,
|
|
1504
|
+
clientCode,
|
|
1505
|
+
permissionsCode
|
|
721
1506
|
};
|
|
722
1507
|
}
|
|
723
1508
|
async function generateApplication(graph, options) {
|
|
@@ -725,9 +1510,32 @@ async function generateApplication(graph, options) {
|
|
|
725
1510
|
await mkdir(options.outDir, { recursive: true });
|
|
726
1511
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
727
1512
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
728
|
-
await
|
|
729
|
-
await
|
|
730
|
-
|
|
1513
|
+
await writeFileAtomic(applicationPath, rendered.applicationCode);
|
|
1514
|
+
await writeFileAtomic(manifestPath, rendered.manifestJson);
|
|
1515
|
+
const written = [applicationPath, manifestPath];
|
|
1516
|
+
if (rendered.clientCode) {
|
|
1517
|
+
const clientPath = join2(options.outDir, "client.ts");
|
|
1518
|
+
await writeFileAtomic(clientPath, rendered.clientCode);
|
|
1519
|
+
written.push(clientPath);
|
|
1520
|
+
}
|
|
1521
|
+
if (rendered.permissionsCode) {
|
|
1522
|
+
const permissionsPath = join2(options.outDir, "permissions.ts");
|
|
1523
|
+
await writeFileAtomic(permissionsPath, rendered.permissionsCode);
|
|
1524
|
+
written.push(permissionsPath);
|
|
1525
|
+
}
|
|
1526
|
+
return written;
|
|
1527
|
+
}
|
|
1528
|
+
async function writeFileAtomic(path, content) {
|
|
1529
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1530
|
+
try {
|
|
1531
|
+
await writeFile(temporaryPath, content, "utf8");
|
|
1532
|
+
await rename(temporaryPath, path);
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
await unlink(temporaryPath).catch(() => {
|
|
1535
|
+
return;
|
|
1536
|
+
});
|
|
1537
|
+
throw error;
|
|
1538
|
+
}
|
|
731
1539
|
}
|
|
732
1540
|
function factoryOfScope(scope) {
|
|
733
1541
|
return scope === "application" ? "services" : scope;
|
|
@@ -864,6 +1672,81 @@ class ModuleGenerator {
|
|
|
864
1672
|
}
|
|
865
1673
|
if (route.command)
|
|
866
1674
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1675
|
+
if (route.guards && route.guards.length > 0) {
|
|
1676
|
+
fields.push(`guards: ${JSON.stringify(route.guards)}`);
|
|
1677
|
+
}
|
|
1678
|
+
if (route.canMatch && route.canMatch.length > 0) {
|
|
1679
|
+
fields.push(`canMatch: ${JSON.stringify(route.canMatch)}`);
|
|
1680
|
+
}
|
|
1681
|
+
if (route.canDeactivate && route.canDeactivate.length > 0) {
|
|
1682
|
+
fields.push(`canDeactivate: ${JSON.stringify(route.canDeactivate)}`);
|
|
1683
|
+
}
|
|
1684
|
+
if (route.resolvers && Object.keys(route.resolvers).length > 0) {
|
|
1685
|
+
fields.push(`resolvers: ${JSON.stringify(route.resolvers)}`);
|
|
1686
|
+
}
|
|
1687
|
+
if (route.redirectTo) {
|
|
1688
|
+
fields.push(`redirectTo: ${JSON.stringify(route.redirectTo)}`);
|
|
1689
|
+
}
|
|
1690
|
+
if (route.pathMatch) {
|
|
1691
|
+
fields.push(`pathMatch: ${JSON.stringify(route.pathMatch)}`);
|
|
1692
|
+
}
|
|
1693
|
+
if (route.paramTransforms && Object.keys(route.paramTransforms).length > 0) {
|
|
1694
|
+
fields.push(`paramTransforms: ${JSON.stringify(route.paramTransforms)}`);
|
|
1695
|
+
}
|
|
1696
|
+
if (route.paramDefaults && Object.keys(route.paramDefaults).length > 0) {
|
|
1697
|
+
fields.push(`paramDefaults: ${JSON.stringify(route.paramDefaults)}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (route.queryTransforms && Object.keys(route.queryTransforms).length > 0) {
|
|
1700
|
+
fields.push(`queryTransforms: ${JSON.stringify(route.queryTransforms)}`);
|
|
1701
|
+
}
|
|
1702
|
+
if (route.queryDefaults && Object.keys(route.queryDefaults).length > 0) {
|
|
1703
|
+
fields.push(`queryDefaults: ${JSON.stringify(route.queryDefaults)}`);
|
|
1704
|
+
}
|
|
1705
|
+
if (route.title) {
|
|
1706
|
+
fields.push(`title: ${JSON.stringify(route.title)}`);
|
|
1707
|
+
}
|
|
1708
|
+
if (route.data && Object.keys(route.data).length > 0) {
|
|
1709
|
+
fields.push(`data: ${JSON.stringify(route.data)}`);
|
|
1710
|
+
}
|
|
1711
|
+
const invokerArgs = (route.handlerParams ?? []).map((hp) => {
|
|
1712
|
+
if (hp.kind === "param") {
|
|
1713
|
+
const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
|
|
1714
|
+
const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
|
|
1715
|
+
if (hp.transform === "number") {
|
|
1716
|
+
return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
|
|
1717
|
+
}
|
|
1718
|
+
if (hp.transform === "boolean") {
|
|
1719
|
+
return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
|
|
1720
|
+
}
|
|
1721
|
+
if (hp.transform === "string") {
|
|
1722
|
+
return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
|
|
1723
|
+
}
|
|
1724
|
+
return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
|
|
1725
|
+
}
|
|
1726
|
+
if (hp.kind === "query") {
|
|
1727
|
+
const accessor = `req.query?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
|
|
1728
|
+
const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
|
|
1729
|
+
if (hp.transform === "number") {
|
|
1730
|
+
return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
|
|
1731
|
+
}
|
|
1732
|
+
if (hp.transform === "boolean") {
|
|
1733
|
+
return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
|
|
1734
|
+
}
|
|
1735
|
+
if (hp.transform === "string") {
|
|
1736
|
+
return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
|
|
1737
|
+
}
|
|
1738
|
+
return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
|
|
1739
|
+
}
|
|
1740
|
+
if (hp.kind === "body")
|
|
1741
|
+
return "req.body";
|
|
1742
|
+
if (hp.kind === "headers")
|
|
1743
|
+
return "req.headers";
|
|
1744
|
+
if (hp.kind === "context")
|
|
1745
|
+
return "(req.context ?? req)";
|
|
1746
|
+
return "undefined";
|
|
1747
|
+
});
|
|
1748
|
+
const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
|
|
1749
|
+
fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
|
|
867
1750
|
return `{ ${fields.join(", ")} }`;
|
|
868
1751
|
});
|
|
869
1752
|
return [
|
|
@@ -909,11 +1792,24 @@ ${indent(item, 2)}`).join(",")}
|
|
|
909
1792
|
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
910
1793
|
const lines = [];
|
|
911
1794
|
const returns = new Map;
|
|
1795
|
+
const multiGroups = new Map;
|
|
912
1796
|
for (const provider of providers) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1797
|
+
if (provider.multi) {
|
|
1798
|
+
const emitted = this.emitProvider(provider, kind, true);
|
|
1799
|
+
if (emitted.constLine)
|
|
1800
|
+
lines.push(emitted.constLine);
|
|
1801
|
+
const list = multiGroups.get(emitted.key) ?? [];
|
|
1802
|
+
list.push(emitted.expr);
|
|
1803
|
+
multiGroups.set(emitted.key, list);
|
|
1804
|
+
} else {
|
|
1805
|
+
const emitted = this.emitProvider(provider, kind, false);
|
|
1806
|
+
if (emitted.constLine)
|
|
1807
|
+
lines.push(emitted.constLine);
|
|
1808
|
+
returns.set(emitted.key, emitted.expr);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
for (const [key, exprs] of multiGroups) {
|
|
1812
|
+
returns.set(key, `[${exprs.join(", ")}]`);
|
|
917
1813
|
}
|
|
918
1814
|
for (const controller of controllers) {
|
|
919
1815
|
const emitted = this.emitController(controller, kind);
|
|
@@ -925,34 +1821,40 @@ ${indent(item, 2)}`).join(",")}
|
|
|
925
1821
|
return lines.join(`
|
|
926
1822
|
`);
|
|
927
1823
|
}
|
|
928
|
-
emitProvider(provider, kind) {
|
|
1824
|
+
emitProvider(provider, kind, isMulti = false) {
|
|
929
1825
|
const key = camelName(provider.token);
|
|
930
1826
|
switch (provider.kind) {
|
|
931
1827
|
case "class": {
|
|
932
1828
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
|
|
933
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
934
|
-
const local = this.localVar(provider.token, kind);
|
|
1829
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
|
|
1830
|
+
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
935
1831
|
return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
|
|
936
1832
|
}
|
|
937
1833
|
case "value": {
|
|
938
1834
|
const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
|
|
939
|
-
const local = this.localVar(provider.token, kind);
|
|
1835
|
+
const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
940
1836
|
return { constLine: `const ${local} = ${expr};`, key, expr: local };
|
|
941
1837
|
}
|
|
942
1838
|
case "factory": {
|
|
1839
|
+
if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
|
|
1840
|
+
const tokenIdent = this.imports.add(provider.token, provider.importPath);
|
|
1841
|
+
const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
1842
|
+
const constLine = `const ${local2} = typeof ${tokenIdent} === "object" && ${tokenIdent} && "factory" in ${tokenIdent} && typeof (${tokenIdent} as any).factory === "function" ? (${tokenIdent} as any).factory() : undefined;`;
|
|
1843
|
+
return { constLine, key, expr: local2 };
|
|
1844
|
+
}
|
|
943
1845
|
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
|
|
944
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
945
|
-
const local = this.localVar(provider.token, kind);
|
|
1846
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
|
|
1847
|
+
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
946
1848
|
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
947
1849
|
}
|
|
948
1850
|
case "existing": {
|
|
949
|
-
return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind) };
|
|
1851
|
+
return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
|
|
950
1852
|
}
|
|
951
1853
|
}
|
|
952
1854
|
}
|
|
953
1855
|
emitController(controller, kind) {
|
|
954
1856
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
955
|
-
const args = controller.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
|
|
1857
|
+
const args = controller.deps.map((dep) => this.depExpr(dep, kind, controller.optionalDeps?.includes(dep))).join(", ");
|
|
956
1858
|
const key = camelName(controller.className);
|
|
957
1859
|
const local = this.localVar(controller.className, kind);
|
|
958
1860
|
return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
|
|
@@ -972,7 +1874,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
972
1874
|
locals.set(token, local);
|
|
973
1875
|
return local;
|
|
974
1876
|
}
|
|
975
|
-
depExpr(token, kind) {
|
|
1877
|
+
depExpr(token, kind, isOptional = false) {
|
|
976
1878
|
if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
|
|
977
1879
|
return "ctx";
|
|
978
1880
|
if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
|
|
@@ -983,7 +1885,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
983
1885
|
return this.locals[kind].get(token) ?? camelName(token);
|
|
984
1886
|
}
|
|
985
1887
|
if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
|
|
986
|
-
return this.depExpr(own.useExisting ?? token, kind);
|
|
1888
|
+
return this.depExpr(own.useExisting ?? token, kind, isOptional);
|
|
987
1889
|
}
|
|
988
1890
|
if (kind === "services") {
|
|
989
1891
|
return `services.${camelName(token)}`;
|
|
@@ -998,9 +1900,18 @@ ${indent(item, 2)}`).join(",")}
|
|
|
998
1900
|
return `imported.${importName}.${camelName(token)}`;
|
|
999
1901
|
return `imported.${importName}.${camelName(token)}`;
|
|
1000
1902
|
}
|
|
1903
|
+
for (const mod of this.graph.modules) {
|
|
1904
|
+
const rootProv = mod.providers.find((p) => p.token === token && p.providedIn === "root");
|
|
1905
|
+
if (rootProv) {
|
|
1906
|
+
return `imported.${mod.name}.${camelName(token)}`;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (isOptional && !this.graph.externalTokens.includes(token)) {
|
|
1910
|
+
return "undefined";
|
|
1911
|
+
}
|
|
1001
1912
|
if (kind === "services")
|
|
1002
|
-
return `deps.${camelName(token)}`;
|
|
1003
|
-
return `services.${camelName(token)}`;
|
|
1913
|
+
return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
|
|
1914
|
+
return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
|
|
1004
1915
|
}
|
|
1005
1916
|
}
|
|
1006
1917
|
function orderProviders(providers) {
|
|
@@ -1022,46 +1933,266 @@ function orderProviders(providers) {
|
|
|
1022
1933
|
}
|
|
1023
1934
|
return result;
|
|
1024
1935
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
{
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1936
|
+
function renderClient(graph, _options) {
|
|
1937
|
+
const controllerEntries = [];
|
|
1938
|
+
const allRoutes = [];
|
|
1939
|
+
for (const module of graph.modules) {
|
|
1940
|
+
for (const controller of module.controllers) {
|
|
1941
|
+
const controllerKey = camelName(controller.className.replace(/Controller$/, ""));
|
|
1942
|
+
const routeMethods = [];
|
|
1943
|
+
for (const route of controller.routes) {
|
|
1944
|
+
const fullPath = joinRoutePaths(controller.path, route.path);
|
|
1945
|
+
allRoutes.push({
|
|
1946
|
+
method: route.method,
|
|
1947
|
+
path: fullPath,
|
|
1948
|
+
controller: controller.className,
|
|
1949
|
+
handler: route.handler,
|
|
1950
|
+
command: route.command,
|
|
1951
|
+
guards: route.guards,
|
|
1952
|
+
canMatch: route.canMatch,
|
|
1953
|
+
canDeactivate: route.canDeactivate,
|
|
1954
|
+
resolvers: route.resolvers,
|
|
1955
|
+
redirectTo: route.redirectTo,
|
|
1956
|
+
pathMatch: route.pathMatch,
|
|
1957
|
+
paramTransforms: route.paramTransforms,
|
|
1958
|
+
paramDefaults: route.paramDefaults,
|
|
1959
|
+
queryTransforms: route.queryTransforms,
|
|
1960
|
+
queryDefaults: route.queryDefaults,
|
|
1961
|
+
title: route.title,
|
|
1962
|
+
data: route.data
|
|
1963
|
+
});
|
|
1964
|
+
routeMethods.push(`
|
|
1965
|
+
${route.handler}: (options: {
|
|
1966
|
+
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>"};
|
|
1967
|
+
query?: Record<string, unknown>;
|
|
1968
|
+
body?: unknown;
|
|
1969
|
+
headers?: Record<string, string>;
|
|
1970
|
+
}${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : " = {}"}) => request(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, options),`);
|
|
1971
|
+
}
|
|
1972
|
+
controllerEntries.push(`
|
|
1973
|
+
${controllerKey}: {${routeMethods.join("")}
|
|
1974
|
+
},`);
|
|
1975
|
+
}
|
|
1047
1976
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1977
|
+
return [
|
|
1978
|
+
HEADER,
|
|
1979
|
+
"",
|
|
1980
|
+
"export interface ClientRequestOptions {",
|
|
1981
|
+
" params?: Record<string, string | number>;",
|
|
1982
|
+
" query?: Record<string, unknown>;",
|
|
1983
|
+
" body?: unknown;",
|
|
1984
|
+
" headers?: Record<string, string>;",
|
|
1985
|
+
"}",
|
|
1986
|
+
"",
|
|
1987
|
+
"export type HttpInterceptorFn = (",
|
|
1988
|
+
" req: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
1989
|
+
" next: (req: { method: string; url: string; headers: Record<string, string>; body?: unknown }) => Promise<Response>,",
|
|
1990
|
+
") => Promise<Response>;",
|
|
1991
|
+
"",
|
|
1992
|
+
"export interface ApiClientConfig {",
|
|
1993
|
+
" baseUrl?: string;",
|
|
1994
|
+
" fetch?: typeof fetch;",
|
|
1995
|
+
" headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);",
|
|
1996
|
+
" interceptors?: HttpInterceptorFn[];",
|
|
1997
|
+
"}",
|
|
1998
|
+
"",
|
|
1999
|
+
"export const API_ROUTES = " + JSON.stringify(allRoutes, null, 2) + " as const;",
|
|
2000
|
+
"",
|
|
2001
|
+
"export type AppRoutePath = typeof API_ROUTES[number]['path'];",
|
|
2002
|
+
"",
|
|
2003
|
+
"/**",
|
|
2004
|
+
" * Type-safe URL builder replacing route path parameters and appending query parameters.",
|
|
2005
|
+
" */",
|
|
2006
|
+
"export function buildRouteUrl(",
|
|
2007
|
+
" path: string,",
|
|
2008
|
+
" params?: Record<string, string | number>,",
|
|
2009
|
+
" query?: Record<string, unknown>,",
|
|
2010
|
+
"): string {",
|
|
2011
|
+
" let url = path;",
|
|
2012
|
+
" if (params) {",
|
|
2013
|
+
" for (const [key, value] of Object.entries(params)) {",
|
|
2014
|
+
" url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
|
|
2015
|
+
" }",
|
|
2016
|
+
" }",
|
|
2017
|
+
" if (query) {",
|
|
2018
|
+
" const searchParams = new URLSearchParams();",
|
|
2019
|
+
" for (const [k, v] of Object.entries(query)) {",
|
|
2020
|
+
" if (v !== undefined && v !== null) searchParams.set(k, String(v));",
|
|
2021
|
+
" }",
|
|
2022
|
+
" const qs = searchParams.toString();",
|
|
2023
|
+
' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
|
|
2024
|
+
" }",
|
|
2025
|
+
" return url;",
|
|
2026
|
+
"}",
|
|
2027
|
+
"",
|
|
2028
|
+
"export function createApiClient(config: ApiClientConfig = {}) {",
|
|
2029
|
+
" const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
|
|
2030
|
+
' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
|
|
2031
|
+
"",
|
|
2032
|
+
" async function request<T = unknown>(",
|
|
2033
|
+
" method: string,",
|
|
2034
|
+
" path: string,",
|
|
2035
|
+
" options: ClientRequestOptions = {},",
|
|
2036
|
+
" ): Promise<T> {",
|
|
2037
|
+
" let url = `${baseUrl}${path}`;",
|
|
2038
|
+
" if (options.params) {",
|
|
2039
|
+
" for (const [key, value] of Object.entries(options.params)) {",
|
|
2040
|
+
" url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
|
|
2041
|
+
" }",
|
|
2042
|
+
" }",
|
|
2043
|
+
" if (options.query) {",
|
|
2044
|
+
" const searchParams = new URLSearchParams();",
|
|
2045
|
+
" for (const [k, v] of Object.entries(options.query)) {",
|
|
2046
|
+
" if (v !== undefined && v !== null) searchParams.set(k, String(v));",
|
|
2047
|
+
" }",
|
|
2048
|
+
" const qs = searchParams.toString();",
|
|
2049
|
+
' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
|
|
2050
|
+
" }",
|
|
2051
|
+
' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
|
|
2052
|
+
" const headers: Record<string, string> = {",
|
|
2053
|
+
' "content-type": "application/json",',
|
|
2054
|
+
" ...customHeaders,",
|
|
2055
|
+
" ...options.headers,",
|
|
2056
|
+
" };",
|
|
2057
|
+
" const interceptors = config.interceptors ?? [];",
|
|
2058
|
+
" const executeChain = (",
|
|
2059
|
+
" index: number,",
|
|
2060
|
+
" reqPayload: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
|
|
2061
|
+
" ): Promise<Response> => {",
|
|
2062
|
+
" if (index < interceptors.length) {",
|
|
2063
|
+
" return interceptors[index](reqPayload, (nextPayload) => executeChain(index + 1, nextPayload));",
|
|
2064
|
+
" }",
|
|
2065
|
+
" return fetcher(reqPayload.url, {",
|
|
2066
|
+
" method: reqPayload.method,",
|
|
2067
|
+
" headers: reqPayload.headers,",
|
|
2068
|
+
" body: reqPayload.body !== undefined ? JSON.stringify(reqPayload.body) : undefined,",
|
|
2069
|
+
" });",
|
|
2070
|
+
" };",
|
|
2071
|
+
" const response = await executeChain(0, { method, url, headers, body: options.body });",
|
|
2072
|
+
" if (!response.ok) {",
|
|
2073
|
+
" const errBody = await response.text();",
|
|
2074
|
+
" throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
|
|
2075
|
+
" }",
|
|
2076
|
+
' const contentType = response.headers?.get("content-type") ?? "";',
|
|
2077
|
+
' if (contentType.includes("application/json")) {',
|
|
2078
|
+
" return response.json() as Promise<T>;",
|
|
2079
|
+
" }",
|
|
2080
|
+
" return response.text() as Promise<T>;",
|
|
2081
|
+
" }",
|
|
2082
|
+
"",
|
|
2083
|
+
" return {",
|
|
2084
|
+
" request,",
|
|
2085
|
+
" buildRouteUrl,",
|
|
2086
|
+
" routes: API_ROUTES,",
|
|
2087
|
+
...controllerEntries,
|
|
2088
|
+
" };",
|
|
2089
|
+
"}",
|
|
2090
|
+
"",
|
|
2091
|
+
"export type ApiClient = ReturnType<typeof createApiClient>;",
|
|
2092
|
+
""
|
|
2093
|
+
].join(`
|
|
2094
|
+
`);
|
|
2095
|
+
}
|
|
2096
|
+
function renderPermissions(graph) {
|
|
2097
|
+
const permissions = new Set;
|
|
2098
|
+
const bindings = [];
|
|
2099
|
+
for (const module of graph.modules) {
|
|
2100
|
+
for (const command of module.commands) {
|
|
2101
|
+
if (command.permission)
|
|
2102
|
+
permissions.add(command.permission);
|
|
2103
|
+
}
|
|
2104
|
+
for (const controller of module.controllers) {
|
|
2105
|
+
for (const route of controller.routes) {
|
|
2106
|
+
let perm;
|
|
2107
|
+
if (route.command) {
|
|
2108
|
+
const cmd = module.commands.find((c) => c.className === route.command);
|
|
2109
|
+
perm = cmd?.permission;
|
|
2110
|
+
}
|
|
2111
|
+
if (perm)
|
|
2112
|
+
permissions.add(perm);
|
|
2113
|
+
bindings.push({
|
|
2114
|
+
method: route.method,
|
|
2115
|
+
path: joinRoutePaths(controller.path, route.path),
|
|
2116
|
+
controller: controller.className,
|
|
2117
|
+
handler: route.handler,
|
|
2118
|
+
command: route.command,
|
|
2119
|
+
permission: perm
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const sortedPerms = [...permissions].sort();
|
|
2125
|
+
const enumEntries = sortedPerms.map((perm) => {
|
|
2126
|
+
const key = pascalName(perm.replace(/[^A-Za-z0-9]+/g, " "));
|
|
2127
|
+
return ` ${key}: ${JSON.stringify(perm)},`;
|
|
2128
|
+
});
|
|
2129
|
+
return [
|
|
2130
|
+
HEADER,
|
|
2131
|
+
"",
|
|
2132
|
+
"export const AppPermissions = {",
|
|
2133
|
+
...enumEntries,
|
|
2134
|
+
"} as const;",
|
|
2135
|
+
"",
|
|
2136
|
+
"export type AppPermission = (typeof AppPermissions)[keyof typeof AppPermissions];",
|
|
2137
|
+
"",
|
|
2138
|
+
"export interface RoutePermissionBinding {",
|
|
2139
|
+
" method: string;",
|
|
2140
|
+
" path: string;",
|
|
2141
|
+
" controller: string;",
|
|
2142
|
+
" handler: string;",
|
|
2143
|
+
" command?: string;",
|
|
2144
|
+
" permission?: string;",
|
|
2145
|
+
"}",
|
|
2146
|
+
"",
|
|
2147
|
+
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
2148
|
+
"",
|
|
2149
|
+
"export function hasPermission(granted: string[], required: AppPermission | string): boolean {",
|
|
2150
|
+
' return granted.includes("*") || granted.includes(required);',
|
|
2151
|
+
"}",
|
|
2152
|
+
""
|
|
2153
|
+
].join(`
|
|
2154
|
+
`);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/profiles.ts
|
|
2158
|
+
var MODULAR_MONOLITH_RULES = [
|
|
2159
|
+
{
|
|
2160
|
+
sourceTag: "type:feature",
|
|
2161
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
sourceTag: "type:root",
|
|
2165
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
sourceTag: "type:app",
|
|
2169
|
+
onlyDependOnLibsWithTags: ["type:feature", "type:core", "type:shared", "type:domain"]
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
sourceTag: "type:core",
|
|
2173
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
sourceTag: "type:shared",
|
|
2177
|
+
bannedDependenciesWithTags: ["type:feature", "type:core", "type:root", "type:app"]
|
|
2178
|
+
}
|
|
2179
|
+
];
|
|
2180
|
+
var ANGULAR_ENTERPRISE_RULES = [
|
|
2181
|
+
{
|
|
2182
|
+
sourceTag: "type:feature",
|
|
2183
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
sourceTag: "type:ui",
|
|
2187
|
+
bannedDependenciesWithTags: ["type:feature", "type:root", "type:app"]
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
sourceTag: "type:data-access",
|
|
2191
|
+
bannedDependenciesWithTags: ["type:feature", "type:ui", "type:root", "type:app"]
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
sourceTag: "type:util",
|
|
2195
|
+
bannedDependenciesWithTags: ["type:feature", "type:ui", "type:data-access", "type:root", "type:app"]
|
|
1065
2196
|
},
|
|
1066
2197
|
{
|
|
1067
2198
|
sourceTag: "type:shared",
|
|
@@ -1196,6 +2327,53 @@ var SCOPE_LIFETIME_RANK = {
|
|
|
1196
2327
|
request: 1,
|
|
1197
2328
|
job: 1
|
|
1198
2329
|
};
|
|
2330
|
+
var COMPILER_DIAGNOSTIC_CODES = {
|
|
2331
|
+
"circular-dependency": { code: "SC1001", docsUrl: "https://supacloud.dev/errors/SC1001" },
|
|
2332
|
+
"scope-violation": { code: "SC1002", docsUrl: "https://supacloud.dev/errors/SC1002" },
|
|
2333
|
+
"module-boundary-violation": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
|
|
2334
|
+
"module-boundary": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
|
|
2335
|
+
"circular-module-import": { code: "SC1004", docsUrl: "https://supacloud.dev/errors/SC1004" },
|
|
2336
|
+
"orphan-module": { code: "SC1005", docsUrl: "https://supacloud.dev/errors/SC1005" },
|
|
2337
|
+
"invalid-boundary-preset": { code: "SC1006", docsUrl: "https://supacloud.dev/errors/SC1006" },
|
|
2338
|
+
"circular-existing-alias": { code: "SC1007", docsUrl: "https://supacloud.dev/errors/SC1007" },
|
|
2339
|
+
"missing-deps": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
|
|
2340
|
+
"unresolved-token": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
|
|
2341
|
+
"duplicate-token": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
|
|
2342
|
+
"duplicate-module": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
|
|
2343
|
+
"disallow-controller-direct-db": { code: "SC2003", docsUrl: "https://supacloud.dev/errors/SC2003" },
|
|
2344
|
+
"self-dependency-violation": { code: "SC2004", docsUrl: "https://supacloud.dev/errors/SC2004" },
|
|
2345
|
+
"skip-self-dependency-violation": { code: "SC2005", docsUrl: "https://supacloud.dev/errors/SC2005" },
|
|
2346
|
+
"export-unprovided-token": { code: "SC2006", docsUrl: "https://supacloud.dev/errors/SC2006" },
|
|
2347
|
+
"unresolved-alias-target": { code: "SC2007", docsUrl: "https://supacloud.dev/errors/SC2007" },
|
|
2348
|
+
"self-referencing-alias": { code: "SC2008", docsUrl: "https://supacloud.dev/errors/SC2008" },
|
|
2349
|
+
"shadowed-route": { code: "SC3001", docsUrl: "https://supacloud.dev/errors/SC3001" },
|
|
2350
|
+
"unresolved-route-redirect": { code: "SC3002", docsUrl: "https://supacloud.dev/errors/SC3002" },
|
|
2351
|
+
"circular-route-redirect": { code: "SC3003", docsUrl: "https://supacloud.dev/errors/SC3003" },
|
|
2352
|
+
"invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
|
|
2353
|
+
"unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
|
|
2354
|
+
"missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
2355
|
+
"duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
|
|
2356
|
+
"missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
|
|
2357
|
+
"unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
|
|
2358
|
+
"malformed-route-path": { code: "SC3010", docsUrl: "https://supacloud.dev/errors/SC3010" },
|
|
2359
|
+
"duplicate-path-param": { code: "SC3011", docsUrl: "https://supacloud.dev/errors/SC3011" },
|
|
2360
|
+
"wildcard-not-trailing": { code: "SC3012", docsUrl: "https://supacloud.dev/errors/SC3012" },
|
|
2361
|
+
"invalid-query-param-name": { code: "SC3013", docsUrl: "https://supacloud.dev/errors/SC3013" },
|
|
2362
|
+
"unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
|
|
2363
|
+
"invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
|
|
2364
|
+
"disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
2365
|
+
"duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
|
|
2366
|
+
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
2367
|
+
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
2368
|
+
"missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
|
|
2369
|
+
"command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
|
|
2370
|
+
"duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
|
|
2371
|
+
"route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
|
|
2372
|
+
"command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
|
|
2373
|
+
"route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
|
|
2374
|
+
"command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
|
|
2375
|
+
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
2376
|
+
};
|
|
1199
2377
|
function validateGraph(graph, options = false) {
|
|
1200
2378
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
1201
2379
|
const diagnostics = [];
|
|
@@ -1207,12 +2385,15 @@ function validateGraph(graph, options = false) {
|
|
|
1207
2385
|
rules: options.moduleBoundaries
|
|
1208
2386
|
});
|
|
1209
2387
|
} catch (err) {
|
|
2388
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["invalid-boundary-preset"];
|
|
1210
2389
|
diagnostics.push({
|
|
1211
2390
|
severity: "error",
|
|
1212
2391
|
code: "invalid-boundary-preset",
|
|
1213
2392
|
message: err instanceof Error ? err.message : String(err),
|
|
1214
2393
|
file: graph.modules[0]?.file,
|
|
1215
|
-
line: graph.modules[0]?.line
|
|
2394
|
+
line: graph.modules[0]?.line,
|
|
2395
|
+
errorCode: meta?.code,
|
|
2396
|
+
docsUrl: meta?.docsUrl
|
|
1216
2397
|
});
|
|
1217
2398
|
}
|
|
1218
2399
|
}
|
|
@@ -1236,17 +2417,43 @@ function validateGraph(graph, options = false) {
|
|
|
1236
2417
|
if (provider)
|
|
1237
2418
|
return { module: imported, provider };
|
|
1238
2419
|
}
|
|
2420
|
+
for (const mod of graph.modules) {
|
|
2421
|
+
const rootProvider = mod.providers.find((p) => p.token === token && p.providedIn === "root");
|
|
2422
|
+
if (rootProvider)
|
|
2423
|
+
return { module: mod, provider: rootProvider };
|
|
2424
|
+
}
|
|
1239
2425
|
return;
|
|
1240
2426
|
}
|
|
1241
|
-
const error = (code, message, file, line) => {
|
|
1242
|
-
|
|
2427
|
+
const error = (code, message, file, line, suggestion) => {
|
|
2428
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
2429
|
+
diagnostics.push({
|
|
2430
|
+
severity: "error",
|
|
2431
|
+
code,
|
|
2432
|
+
message,
|
|
2433
|
+
file,
|
|
2434
|
+
line,
|
|
2435
|
+
suggestion,
|
|
2436
|
+
errorCode: meta?.code,
|
|
2437
|
+
docsUrl: meta?.docsUrl
|
|
2438
|
+
});
|
|
1243
2439
|
};
|
|
1244
|
-
const warn2 = (code, message, file, line) => {
|
|
1245
|
-
|
|
2440
|
+
const warn2 = (code, message, file, line, suggestion) => {
|
|
2441
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
2442
|
+
diagnostics.push({
|
|
2443
|
+
severity: strict ? "error" : "warn",
|
|
2444
|
+
code,
|
|
2445
|
+
message,
|
|
2446
|
+
file,
|
|
2447
|
+
line,
|
|
2448
|
+
suggestion,
|
|
2449
|
+
errorCode: meta?.code,
|
|
2450
|
+
docsUrl: meta?.docsUrl
|
|
2451
|
+
});
|
|
1246
2452
|
};
|
|
1247
2453
|
const modulesByName = new Map;
|
|
1248
2454
|
const commandsByName = new Map;
|
|
1249
2455
|
const routesByKey = new Map;
|
|
2456
|
+
const declaredRoutes = [];
|
|
1250
2457
|
for (const module of graph.modules) {
|
|
1251
2458
|
const previousModule = modulesByName.get(module.name);
|
|
1252
2459
|
if (previousModule) {
|
|
@@ -1266,20 +2473,128 @@ function validateGraph(graph, options = false) {
|
|
|
1266
2473
|
for (const module of graph.modules) {
|
|
1267
2474
|
for (const controller of module.controllers) {
|
|
1268
2475
|
for (const route of controller.routes) {
|
|
1269
|
-
|
|
2476
|
+
if (route.path.includes("//") || controller.path.includes("//")) {
|
|
2477
|
+
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.");
|
|
2478
|
+
} else if (/(^|\/):(\/|$)/.test(route.path) || route.path.endsWith("/:")) {
|
|
2479
|
+
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').");
|
|
2480
|
+
} else if (route.path.includes("?") || route.path.includes("#")) {
|
|
2481
|
+
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.");
|
|
2482
|
+
}
|
|
2483
|
+
if (route.path.includes("**")) {
|
|
2484
|
+
const segments = route.path.split("/").filter(Boolean);
|
|
2485
|
+
const wildcardIdx = segments.indexOf("**");
|
|
2486
|
+
if (wildcardIdx !== -1 && wildcardIdx !== segments.length - 1) {
|
|
2487
|
+
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("/")}/**'.`);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const fullPath = joinRoutePaths2(controller.path, route.path);
|
|
2491
|
+
const rawFullPath = joinRawRoutePaths(controller.path, route.path);
|
|
1270
2492
|
const key = `${route.method} ${fullPath}`;
|
|
2493
|
+
const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
|
|
2494
|
+
if (openApiMatch) {
|
|
2495
|
+
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]}'.`);
|
|
2496
|
+
}
|
|
1271
2497
|
const previous = routesByKey.get(key);
|
|
1272
2498
|
if (previous) {
|
|
1273
2499
|
error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
|
|
1274
2500
|
} else {
|
|
1275
2501
|
routesByKey.set(key, { module, controller });
|
|
1276
2502
|
}
|
|
2503
|
+
for (const prev of declaredRoutes) {
|
|
2504
|
+
if (prev.method === route.method && isRouteShadowed(prev.rawFullPath, rawFullPath)) {
|
|
2505
|
+
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}'.`);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
declaredRoutes.push({ method: route.method, path: route.path, fullPath, rawFullPath, controller, module, handler: route.handler, redirectTo: route.redirectTo });
|
|
1277
2509
|
if (route.command && !module.commands.some((command) => command.className === route.command)) {
|
|
1278
2510
|
error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
1279
2511
|
}
|
|
2512
|
+
if ((route.method === "GET" || route.method === "HEAD") && route.command) {
|
|
2513
|
+
const boundCommand = module.commands.find((c) => c.className === route.command);
|
|
2514
|
+
if (boundCommand && boundCommand.transaction === "required") {
|
|
2515
|
+
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'.`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
1280
2518
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
1281
2519
|
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);
|
|
1282
2520
|
}
|
|
2521
|
+
if (route.redirectTo) {
|
|
2522
|
+
const target = route.redirectTo.replace(/\/+$/, "");
|
|
2523
|
+
const current = fullPath.replace(/\/+$/, "");
|
|
2524
|
+
if (target === current || target === route.path.replace(/\/+$/, "")) {
|
|
2525
|
+
error("circular-route-redirect", `Route ${key} defines circular redirectTo '${route.redirectTo}'`, controller.file);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
const pathParams = route.pathParams ?? [];
|
|
2529
|
+
const seenParams = new Set;
|
|
2530
|
+
for (const p of pathParams) {
|
|
2531
|
+
if (seenParams.has(p)) {
|
|
2532
|
+
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').`);
|
|
2533
|
+
}
|
|
2534
|
+
seenParams.add(p);
|
|
2535
|
+
}
|
|
2536
|
+
const paramBindings = route.paramBindings ?? [];
|
|
2537
|
+
for (const binding of paramBindings) {
|
|
2538
|
+
if (!binding || binding.trim().length === 0) {
|
|
2539
|
+
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}'.`);
|
|
2540
|
+
} else if (/[#?&=/\s]/.test(binding)) {
|
|
2541
|
+
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.`);
|
|
2542
|
+
} else if (!pathParams.includes(binding)) {
|
|
2543
|
+
const suggestion = findClosestMatch(binding, pathParams);
|
|
2544
|
+
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);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (paramBindings.length > 0) {
|
|
2548
|
+
for (const param of pathParams) {
|
|
2549
|
+
if (!paramBindings.includes(param)) {
|
|
2550
|
+
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.`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const queryBindings = route.queryBindings ?? [];
|
|
2555
|
+
const seenQueries = new Set;
|
|
2556
|
+
for (const q of queryBindings) {
|
|
2557
|
+
if (!q || q.trim().length === 0) {
|
|
2558
|
+
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').`);
|
|
2559
|
+
} else if (/[#?&=/\s]/.test(q)) {
|
|
2560
|
+
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.`);
|
|
2561
|
+
} else if (seenQueries.has(q)) {
|
|
2562
|
+
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}.`);
|
|
2563
|
+
}
|
|
2564
|
+
seenQueries.add(q);
|
|
2565
|
+
}
|
|
2566
|
+
if (route.queryDefaults && route.queryTransforms) {
|
|
2567
|
+
for (const [paramName, defVal] of Object.entries(route.queryDefaults)) {
|
|
2568
|
+
const transform = route.queryTransforms[paramName];
|
|
2569
|
+
if (transform === "number" && typeof defVal !== "number") {
|
|
2570
|
+
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'.`);
|
|
2571
|
+
} else if (transform === "boolean" && typeof defVal !== "boolean") {
|
|
2572
|
+
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.`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
|
|
2577
|
+
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().`);
|
|
2578
|
+
}
|
|
2579
|
+
if (route.hasBodyBinding && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
2580
|
+
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().`);
|
|
2581
|
+
} else if (route.hasBodyBinding && !route.body) {
|
|
2582
|
+
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.`);
|
|
2583
|
+
} else if (route.body && !route.hasBodyBinding && !route.command) {
|
|
2584
|
+
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.`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
const handlerMethodMap = new Map;
|
|
2588
|
+
for (const route of controller.routes) {
|
|
2589
|
+
const methods = handlerMethodMap.get(route.handler) ?? [];
|
|
2590
|
+
methods.push(route.method);
|
|
2591
|
+
handlerMethodMap.set(route.handler, methods);
|
|
2592
|
+
}
|
|
2593
|
+
for (const [handler, methods] of handlerMethodMap.entries()) {
|
|
2594
|
+
const uniqueMethods = Array.from(new Set(methods));
|
|
2595
|
+
if (uniqueMethods.length > 1) {
|
|
2596
|
+
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.`);
|
|
2597
|
+
}
|
|
1283
2598
|
}
|
|
1284
2599
|
if (typeof options === "object" && options.disallowControllerDirectDb) {
|
|
1285
2600
|
for (const dep of controller.deps) {
|
|
@@ -1289,6 +2604,73 @@ function validateGraph(graph, options = false) {
|
|
|
1289
2604
|
}
|
|
1290
2605
|
}
|
|
1291
2606
|
}
|
|
2607
|
+
if (controller.selfDeps && controller.selfDeps.length > 0) {
|
|
2608
|
+
for (const dep of controller.selfDeps) {
|
|
2609
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2610
|
+
if (!own) {
|
|
2611
|
+
error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
|
|
2616
|
+
for (const dep of controller.skipSelfDeps) {
|
|
2617
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2618
|
+
if (own) {
|
|
2619
|
+
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().`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const allTargetPaths = declaredRoutes.map((r) => r.rawFullPath);
|
|
2626
|
+
for (const item of declaredRoutes) {
|
|
2627
|
+
if (item.redirectTo) {
|
|
2628
|
+
const target = item.redirectTo;
|
|
2629
|
+
if (target.startsWith("/") && !target.startsWith("//")) {
|
|
2630
|
+
const normalizedTarget = target.replace(/\/+$/, "") || "/";
|
|
2631
|
+
const matchesTarget = declaredRoutes.some((candidate) => {
|
|
2632
|
+
if (candidate.rawFullPath === normalizedTarget)
|
|
2633
|
+
return true;
|
|
2634
|
+
return routeMatchesTarget(candidate.rawFullPath, normalizedTarget);
|
|
2635
|
+
});
|
|
2636
|
+
if (!matchesTarget) {
|
|
2637
|
+
const suggestion = findClosestMatch(normalizedTarget, allTargetPaths);
|
|
2638
|
+
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);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const routeByRawPath = new Map;
|
|
2644
|
+
for (const item of declaredRoutes) {
|
|
2645
|
+
if (!routeByRawPath.has(item.rawFullPath)) {
|
|
2646
|
+
routeByRawPath.set(item.rawFullPath, item);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const reportedRedirectCycles = new Set;
|
|
2650
|
+
for (const item of declaredRoutes) {
|
|
2651
|
+
if (item.redirectTo) {
|
|
2652
|
+
const chain = [item.rawFullPath];
|
|
2653
|
+
let curr = item;
|
|
2654
|
+
while (curr && curr.redirectTo) {
|
|
2655
|
+
const target = curr.redirectTo.replace(/\/+$/, "") || "/";
|
|
2656
|
+
if (chain.includes(target)) {
|
|
2657
|
+
const cycle = [...chain.slice(chain.indexOf(target)), target];
|
|
2658
|
+
if (cycle.length > 2) {
|
|
2659
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
2660
|
+
if (!reportedRedirectCycles.has(cycleKey)) {
|
|
2661
|
+
reportedRedirectCycles.add(cycleKey);
|
|
2662
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-route-redirect"];
|
|
2663
|
+
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.");
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
chain.push(target);
|
|
2669
|
+
const next = routeByRawPath.get(target);
|
|
2670
|
+
if (!next || !next.redirectTo)
|
|
2671
|
+
break;
|
|
2672
|
+
curr = next;
|
|
2673
|
+
}
|
|
1292
2674
|
}
|
|
1293
2675
|
}
|
|
1294
2676
|
for (const module of graph.modules) {
|
|
@@ -1296,33 +2678,58 @@ function validateGraph(graph, options = false) {
|
|
|
1296
2678
|
for (const provider of module.providers) {
|
|
1297
2679
|
const first = seen.get(provider.token);
|
|
1298
2680
|
if (first) {
|
|
1299
|
-
|
|
2681
|
+
if (first.multi && provider.multi) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
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).");
|
|
1300
2685
|
} else {
|
|
1301
2686
|
seen.set(provider.token, provider);
|
|
1302
2687
|
}
|
|
1303
2688
|
}
|
|
1304
2689
|
for (const provider of module.providers) {
|
|
2690
|
+
if (provider.selfDeps && provider.selfDeps.length > 0) {
|
|
2691
|
+
for (const dep of provider.selfDeps) {
|
|
2692
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2693
|
+
if (!own) {
|
|
2694
|
+
error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
|
|
2699
|
+
for (const dep of provider.skipSelfDeps) {
|
|
2700
|
+
const own = module.providers.find((p) => p.token === dep);
|
|
2701
|
+
if (own) {
|
|
2702
|
+
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().`);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
1305
2706
|
for (const dep of provider.deps) {
|
|
2707
|
+
const isOptional = provider.optionalDeps?.includes(dep);
|
|
1306
2708
|
const resolved = resolveDep(module, dep);
|
|
1307
2709
|
if (!resolved) {
|
|
2710
|
+
if (isOptional) {
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
1308
2713
|
if (!graph.externalTokens.includes(dep)) {
|
|
1309
2714
|
if (globalProviders.has(dep)) {
|
|
1310
2715
|
const owner = globalProviders.get(dep);
|
|
1311
|
-
error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line);
|
|
2716
|
+
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' }).`);
|
|
2717
|
+
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
2718
|
+
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: () => ... }).`);
|
|
1312
2719
|
} else {
|
|
1313
|
-
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line);
|
|
2720
|
+
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' }).`);
|
|
1314
2721
|
}
|
|
1315
2722
|
}
|
|
1316
2723
|
continue;
|
|
1317
2724
|
}
|
|
1318
2725
|
if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
|
|
1319
|
-
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line);
|
|
2726
|
+
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.`);
|
|
1320
2727
|
}
|
|
1321
2728
|
}
|
|
1322
2729
|
}
|
|
1323
2730
|
for (const command of module.commands) {
|
|
1324
2731
|
if (!command.permission) {
|
|
1325
|
-
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
|
|
2732
|
+
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.");
|
|
1326
2733
|
}
|
|
1327
2734
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
1328
2735
|
const caps = options.commandCapabilities;
|
|
@@ -1375,18 +2782,116 @@ function validateGraph(graph, options = false) {
|
|
|
1375
2782
|
}
|
|
1376
2783
|
}
|
|
1377
2784
|
}
|
|
2785
|
+
const referencedTokens = new Set;
|
|
2786
|
+
for (const mod of graph.modules) {
|
|
2787
|
+
for (const exp of mod.exports)
|
|
2788
|
+
referencedTokens.add(exp);
|
|
2789
|
+
for (const ctrl of mod.controllers) {
|
|
2790
|
+
for (const d of ctrl.deps ?? [])
|
|
2791
|
+
referencedTokens.add(d);
|
|
2792
|
+
for (const d of ctrl.optionalDeps ?? [])
|
|
2793
|
+
referencedTokens.add(d);
|
|
2794
|
+
for (const d of ctrl.selfDeps ?? [])
|
|
2795
|
+
referencedTokens.add(d);
|
|
2796
|
+
for (const d of ctrl.skipSelfDeps ?? [])
|
|
2797
|
+
referencedTokens.add(d);
|
|
2798
|
+
}
|
|
2799
|
+
for (const p of mod.providers) {
|
|
2800
|
+
for (const d of p.deps ?? [])
|
|
2801
|
+
referencedTokens.add(d);
|
|
2802
|
+
for (const d of p.optionalDeps ?? [])
|
|
2803
|
+
referencedTokens.add(d);
|
|
2804
|
+
for (const d of p.selfDeps ?? [])
|
|
2805
|
+
referencedTokens.add(d);
|
|
2806
|
+
for (const d of p.skipSelfDeps ?? [])
|
|
2807
|
+
referencedTokens.add(d);
|
|
2808
|
+
if (p.useExisting)
|
|
2809
|
+
referencedTokens.add(p.useExisting);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
for (const mod of graph.modules) {
|
|
2813
|
+
for (const provider of mod.providers) {
|
|
2814
|
+
if (provider.providedIn === "root" && !provider.multi && !referencedTokens.has(provider.token) && !provider.exported) {
|
|
2815
|
+
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.`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
for (const module of graph.modules) {
|
|
2820
|
+
for (const expToken of module.exports) {
|
|
2821
|
+
const resolved = resolveDep(module, expToken);
|
|
2822
|
+
if (resolved)
|
|
2823
|
+
continue;
|
|
2824
|
+
if (module.imports.includes(expToken))
|
|
2825
|
+
continue;
|
|
2826
|
+
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.`);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
for (const module of graph.modules) {
|
|
2830
|
+
for (const provider of module.providers) {
|
|
2831
|
+
if (provider.useExisting) {
|
|
2832
|
+
const target = provider.useExisting;
|
|
2833
|
+
if (target === provider.token) {
|
|
2834
|
+
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.`);
|
|
2835
|
+
} else {
|
|
2836
|
+
const resolved = resolveDep(module, target);
|
|
2837
|
+
if (!resolved && !graph.externalTokens.includes(target)) {
|
|
2838
|
+
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.`);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
1378
2844
|
diagnostics.push(...detectCycles(graph, resolveDep));
|
|
2845
|
+
diagnostics.push(...detectExistingAliasCycles(graph, resolveDep));
|
|
1379
2846
|
diagnostics.push(...detectModuleCycles(graph));
|
|
1380
2847
|
if (typeof options === "object" && options.detectOrphanModules) {
|
|
1381
2848
|
diagnostics.push(...detectOrphanModules(graph));
|
|
1382
2849
|
}
|
|
1383
2850
|
return diagnostics;
|
|
1384
2851
|
}
|
|
1385
|
-
function
|
|
2852
|
+
function joinRoutePaths2(prefix, path) {
|
|
1386
2853
|
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1387
2854
|
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1388
2855
|
return normalized.replace(/:[^/]+/g, ":param");
|
|
1389
2856
|
}
|
|
2857
|
+
function joinRawRoutePaths(prefix, path) {
|
|
2858
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
2859
|
+
return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
2860
|
+
}
|
|
2861
|
+
function isRouteShadowed(earlierPath, laterPath) {
|
|
2862
|
+
const earlierSegments = earlierPath.split("/").filter(Boolean);
|
|
2863
|
+
const laterSegments = laterPath.split("/").filter(Boolean);
|
|
2864
|
+
if (earlierSegments.length !== laterSegments.length) {
|
|
2865
|
+
return false;
|
|
2866
|
+
}
|
|
2867
|
+
let hasParamShadowing = false;
|
|
2868
|
+
for (let i = 0;i < earlierSegments.length; i += 1) {
|
|
2869
|
+
const e = earlierSegments[i];
|
|
2870
|
+
const l = laterSegments[i];
|
|
2871
|
+
if (e === l) {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (e.startsWith(":") && !l.startsWith(":")) {
|
|
2875
|
+
hasParamShadowing = true;
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
return false;
|
|
2879
|
+
}
|
|
2880
|
+
return hasParamShadowing;
|
|
2881
|
+
}
|
|
2882
|
+
function routeMatchesTarget(routePattern, targetPath) {
|
|
2883
|
+
const pSegs = routePattern.split("/").filter(Boolean);
|
|
2884
|
+
const tSegs = targetPath.split("/").filter(Boolean);
|
|
2885
|
+
if (pSegs.length !== tSegs.length)
|
|
2886
|
+
return false;
|
|
2887
|
+
for (let i = 0;i < pSegs.length; i += 1) {
|
|
2888
|
+
if (pSegs[i].startsWith(":"))
|
|
2889
|
+
continue;
|
|
2890
|
+
if (pSegs[i] !== tSegs[i])
|
|
2891
|
+
return false;
|
|
2892
|
+
}
|
|
2893
|
+
return true;
|
|
2894
|
+
}
|
|
1390
2895
|
function detectCycles(graph, resolveDep) {
|
|
1391
2896
|
const diagnostics = [];
|
|
1392
2897
|
const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
|
|
@@ -1405,12 +2910,16 @@ function detectCycles(graph, resolveDep) {
|
|
|
1405
2910
|
const cycleKey = cycle.map((item) => nodeId(item)).sort().join("|");
|
|
1406
2911
|
if (!reported.has(cycleKey)) {
|
|
1407
2912
|
reported.add(cycleKey);
|
|
2913
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-dependency"];
|
|
1408
2914
|
diagnostics.push({
|
|
1409
2915
|
severity: "error",
|
|
1410
2916
|
code: "circular-dependency",
|
|
1411
2917
|
message: `provider 循环依赖: ${path}`,
|
|
1412
2918
|
file: ref.provider.file,
|
|
1413
|
-
line: ref.provider.line
|
|
2919
|
+
line: ref.provider.line,
|
|
2920
|
+
suggestion: "Break the cycle by extracting common dependencies into a separate service or injecting @Optional().",
|
|
2921
|
+
errorCode: meta?.code,
|
|
2922
|
+
docsUrl: meta?.docsUrl
|
|
1414
2923
|
});
|
|
1415
2924
|
}
|
|
1416
2925
|
return;
|
|
@@ -1429,6 +2938,47 @@ function detectCycles(graph, resolveDep) {
|
|
|
1429
2938
|
visit(ref);
|
|
1430
2939
|
return diagnostics;
|
|
1431
2940
|
}
|
|
2941
|
+
function detectExistingAliasCycles(graph, resolveDep) {
|
|
2942
|
+
const diagnostics = [];
|
|
2943
|
+
const existingProviders = [];
|
|
2944
|
+
for (const module of graph.modules) {
|
|
2945
|
+
for (const provider of module.providers) {
|
|
2946
|
+
if (provider.useExisting) {
|
|
2947
|
+
existingProviders.push({ module, provider });
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const reported = new Set;
|
|
2952
|
+
for (const start of existingProviders) {
|
|
2953
|
+
const visited = [start.provider.token];
|
|
2954
|
+
let current = start;
|
|
2955
|
+
while (current && current.provider.useExisting) {
|
|
2956
|
+
const targetToken = current.provider.useExisting;
|
|
2957
|
+
if (visited.includes(targetToken)) {
|
|
2958
|
+
const cycle = [...visited.slice(visited.indexOf(targetToken)), targetToken];
|
|
2959
|
+
const cycleKey = [...cycle].sort().join("|");
|
|
2960
|
+
if (!reported.has(cycleKey)) {
|
|
2961
|
+
reported.add(cycleKey);
|
|
2962
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-existing-alias"];
|
|
2963
|
+
diagnostics.push({
|
|
2964
|
+
severity: "error",
|
|
2965
|
+
code: "circular-existing-alias",
|
|
2966
|
+
message: `Provider alias cycle detected in useExisting: ${cycle.join(" -> ")}`,
|
|
2967
|
+
file: start.provider.file,
|
|
2968
|
+
line: start.provider.line,
|
|
2969
|
+
suggestion: "Break the alias cycle by pointing useExisting to a concrete provider instead of a circular alias.",
|
|
2970
|
+
errorCode: meta?.code,
|
|
2971
|
+
docsUrl: meta?.docsUrl
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
break;
|
|
2975
|
+
}
|
|
2976
|
+
visited.push(targetToken);
|
|
2977
|
+
current = resolveDep(current.module, targetToken);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return diagnostics;
|
|
2981
|
+
}
|
|
1432
2982
|
function detectModuleCycles(graph) {
|
|
1433
2983
|
const diagnostics = [];
|
|
1434
2984
|
const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
|
|
@@ -1445,12 +2995,16 @@ function detectModuleCycles(graph) {
|
|
|
1445
2995
|
if (!reported.has(cycleKey)) {
|
|
1446
2996
|
reported.add(cycleKey);
|
|
1447
2997
|
const mod2 = moduleMap.get(name);
|
|
2998
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["circular-module-import"];
|
|
1448
2999
|
diagnostics.push({
|
|
1449
3000
|
severity: "error",
|
|
1450
3001
|
code: "circular-module-import",
|
|
1451
3002
|
message: `Module circular import detected: ${cycle.join(" -> ")}`,
|
|
1452
3003
|
file: mod2?.file,
|
|
1453
|
-
line: mod2?.line
|
|
3004
|
+
line: mod2?.line,
|
|
3005
|
+
suggestion: "Refactor module imports into a unidirectional acyclic graph.",
|
|
3006
|
+
errorCode: meta?.code,
|
|
3007
|
+
docsUrl: meta?.docsUrl
|
|
1454
3008
|
});
|
|
1455
3009
|
}
|
|
1456
3010
|
return;
|
|
@@ -1498,12 +3052,15 @@ function detectOrphanModules(graph) {
|
|
|
1498
3052
|
}
|
|
1499
3053
|
for (const mod of graph.modules) {
|
|
1500
3054
|
if (!reachable.has(mod.name)) {
|
|
3055
|
+
const meta = COMPILER_DIAGNOSTIC_CODES["orphan-module"];
|
|
1501
3056
|
diagnostics.push({
|
|
1502
3057
|
severity: "warn",
|
|
1503
3058
|
code: "orphan-module",
|
|
1504
3059
|
message: `Module '${mod.name}' is declared but not reachable from any root module (${rootModules.map((r) => r.name).join(", ")})`,
|
|
1505
3060
|
file: mod.file,
|
|
1506
|
-
line: mod.line
|
|
3061
|
+
line: mod.line,
|
|
3062
|
+
errorCode: meta?.code,
|
|
3063
|
+
docsUrl: meta?.docsUrl
|
|
1507
3064
|
});
|
|
1508
3065
|
}
|
|
1509
3066
|
}
|
|
@@ -1514,7 +3071,7 @@ function detectOrphanModules(graph) {
|
|
|
1514
3071
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1515
3072
|
import { join as join3 } from "node:path";
|
|
1516
3073
|
async function compileProject(options) {
|
|
1517
|
-
const graph = await analyzeProject(options.rootDir, options.include);
|
|
3074
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
1518
3075
|
const diagnostics = [
|
|
1519
3076
|
...graph.diagnostics ?? [],
|
|
1520
3077
|
...validateGraph(graph, {
|
|
@@ -1533,14 +3090,25 @@ async function compileProject(options) {
|
|
|
1533
3090
|
diagnostic.severity = "error";
|
|
1534
3091
|
}
|
|
1535
3092
|
}
|
|
1536
|
-
const
|
|
3093
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
3094
|
+
const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
|
|
1537
3095
|
rootDir: options.rootDir,
|
|
1538
|
-
outDir: options.outDir
|
|
1539
|
-
|
|
1540
|
-
|
|
3096
|
+
outDir: options.outDir,
|
|
3097
|
+
generateClient: options.generateClient,
|
|
3098
|
+
generatePermissions: options.generatePermissions,
|
|
3099
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
3100
|
+
}) : [];
|
|
3101
|
+
const stats = graph.cacheStats ? {
|
|
3102
|
+
cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
|
|
3103
|
+
changedFiles: [],
|
|
3104
|
+
affectedModules: graph.cacheStats.reanalyzedModules,
|
|
3105
|
+
reanalyzedModules: graph.cacheStats.reanalyzedModules,
|
|
3106
|
+
reusedModules: graph.cacheStats.reusedModules
|
|
3107
|
+
} : undefined;
|
|
3108
|
+
return { diagnostics, graph, written, stats };
|
|
1541
3109
|
}
|
|
1542
3110
|
async function checkProject(options) {
|
|
1543
|
-
const graph = await analyzeProject(options.rootDir, options.include);
|
|
3111
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
1544
3112
|
const diagnostics = [
|
|
1545
3113
|
...graph.diagnostics ?? [],
|
|
1546
3114
|
...validateGraph(graph, {
|
|
@@ -1561,12 +3129,21 @@ async function checkProject(options) {
|
|
|
1561
3129
|
}
|
|
1562
3130
|
const rendered = renderApplication(graph, {
|
|
1563
3131
|
rootDir: options.rootDir,
|
|
1564
|
-
outDir: options.outDir
|
|
3132
|
+
outDir: options.outDir,
|
|
3133
|
+
generateClient: options.generateClient,
|
|
3134
|
+
generatePermissions: options.generatePermissions,
|
|
3135
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
1565
3136
|
});
|
|
1566
3137
|
const expectedFiles = {
|
|
1567
3138
|
"application.ts": rendered.applicationCode,
|
|
1568
3139
|
"app.manifest.json": rendered.manifestJson
|
|
1569
3140
|
};
|
|
3141
|
+
if (rendered.clientCode) {
|
|
3142
|
+
expectedFiles["client.ts"] = rendered.clientCode;
|
|
3143
|
+
}
|
|
3144
|
+
if (rendered.permissionsCode) {
|
|
3145
|
+
expectedFiles["permissions.ts"] = rendered.permissionsCode;
|
|
3146
|
+
}
|
|
1570
3147
|
const mismatches = [];
|
|
1571
3148
|
for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
|
|
1572
3149
|
const diskPath = join3(options.outDir, filename);
|
|
@@ -1587,6 +3164,509 @@ async function checkProject(options) {
|
|
|
1587
3164
|
};
|
|
1588
3165
|
}
|
|
1589
3166
|
|
|
3167
|
+
// src/inspect.ts
|
|
3168
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
3169
|
+
import { join as join4 } from "node:path";
|
|
3170
|
+
function formatGraph(graph) {
|
|
3171
|
+
const lines = [];
|
|
3172
|
+
for (const module of graph.modules) {
|
|
3173
|
+
lines.push(`MODULE ${module.name}`);
|
|
3174
|
+
lines.push(` file: ${module.file}:${module.line}`);
|
|
3175
|
+
lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
|
|
3176
|
+
lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
|
|
3177
|
+
lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
|
|
3178
|
+
lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
|
|
3179
|
+
}
|
|
3180
|
+
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
3181
|
+
return lines.join(`
|
|
3182
|
+
`);
|
|
3183
|
+
}
|
|
3184
|
+
function explainGraph(graph, subject) {
|
|
3185
|
+
const module = graph.modules.find((candidate) => candidate.name === subject);
|
|
3186
|
+
if (module)
|
|
3187
|
+
return explainModule(graph, module);
|
|
3188
|
+
const provider = findProvider(graph, subject);
|
|
3189
|
+
if (provider)
|
|
3190
|
+
return explainProvider(graph, provider.module, provider.provider);
|
|
3191
|
+
if (graph.externalTokens.includes(subject)) {
|
|
3192
|
+
const references = graph.modules.flatMap((candidate) => [
|
|
3193
|
+
...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
|
|
3194
|
+
...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
|
|
3195
|
+
]);
|
|
3196
|
+
return [
|
|
3197
|
+
`EXTERNAL TOKEN ${subject}`,
|
|
3198
|
+
" provided by: platform runtime",
|
|
3199
|
+
` references: ${references.length > 0 ? references.join(", ") : "-"}`
|
|
3200
|
+
].join(`
|
|
3201
|
+
`);
|
|
3202
|
+
}
|
|
3203
|
+
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
3204
|
+
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
3205
|
+
}
|
|
3206
|
+
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
3207
|
+
const checks = [
|
|
3208
|
+
{
|
|
3209
|
+
name: "project-root",
|
|
3210
|
+
ok: existsSync3(rootDir),
|
|
3211
|
+
detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
name: "tsconfig",
|
|
3215
|
+
ok: existsSync3(join4(rootDir, "tsconfig.json")),
|
|
3216
|
+
detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
name: "modules",
|
|
3220
|
+
ok: graph.modules.length > 0,
|
|
3221
|
+
detail: `${graph.modules.length} module(s) discovered`
|
|
3222
|
+
},
|
|
3223
|
+
{
|
|
3224
|
+
name: "generated-artifacts",
|
|
3225
|
+
ok: upToDate,
|
|
3226
|
+
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
3227
|
+
}
|
|
3228
|
+
];
|
|
3229
|
+
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
3230
|
+
return {
|
|
3231
|
+
checks,
|
|
3232
|
+
diagnostics: allDiagnostics,
|
|
3233
|
+
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
function explainModule(graph, module) {
|
|
3237
|
+
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
3238
|
+
return [
|
|
3239
|
+
`MODULE ${module.name}`,
|
|
3240
|
+
` file: ${module.file}:${module.line}`,
|
|
3241
|
+
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
3242
|
+
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
3243
|
+
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
3244
|
+
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
3245
|
+
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`
|
|
3246
|
+
].join(`
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
function explainProvider(graph, module, provider) {
|
|
3250
|
+
const dependents = graph.modules.flatMap((candidate) => [
|
|
3251
|
+
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
3252
|
+
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
3253
|
+
]);
|
|
3254
|
+
return [
|
|
3255
|
+
`PROVIDER ${provider.token}`,
|
|
3256
|
+
` module: ${module.name}`,
|
|
3257
|
+
` file: ${provider.file}:${provider.line}`,
|
|
3258
|
+
` kind: ${provider.kind}`,
|
|
3259
|
+
` scope: ${provider.scope}`,
|
|
3260
|
+
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
3261
|
+
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
3262
|
+
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
3263
|
+
].join(`
|
|
3264
|
+
`);
|
|
3265
|
+
}
|
|
3266
|
+
function findProvider(graph, subject) {
|
|
3267
|
+
for (const module of graph.modules) {
|
|
3268
|
+
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
3269
|
+
if (provider)
|
|
3270
|
+
return { module, provider };
|
|
3271
|
+
}
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
function exportGraphMermaid(graph) {
|
|
3275
|
+
const lines = ["graph TD"];
|
|
3276
|
+
for (const mod of graph.modules) {
|
|
3277
|
+
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3278
|
+
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
3279
|
+
for (const imp of mod.imports) {
|
|
3280
|
+
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3281
|
+
lines.push(` ${safeId} --> ${safeImp}`);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return lines.join(`
|
|
3285
|
+
`);
|
|
3286
|
+
}
|
|
3287
|
+
function exportGraphDot(graph) {
|
|
3288
|
+
const lines = [
|
|
3289
|
+
"digraph ApplicationGraph {",
|
|
3290
|
+
" rankdir=LR;",
|
|
3291
|
+
' node [shape=box, fontname="Helvetica"];'
|
|
3292
|
+
];
|
|
3293
|
+
for (const mod of graph.modules) {
|
|
3294
|
+
lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
|
|
3295
|
+
for (const imp of mod.imports) {
|
|
3296
|
+
lines.push(` "${mod.name}" -> "${imp}";`);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
lines.push("}");
|
|
3300
|
+
return lines.join(`
|
|
3301
|
+
`);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/watch.ts
|
|
3305
|
+
import { watch } from "node:fs";
|
|
3306
|
+
import { relative as relative3, resolve as resolve2 } from "node:path";
|
|
3307
|
+
|
|
3308
|
+
// src/incremental.ts
|
|
3309
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3310
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
3311
|
+
import { relative as relative2, resolve, sep as sep2 } from "node:path";
|
|
3312
|
+
function createDependencyGraphCache() {
|
|
3313
|
+
return {
|
|
3314
|
+
modules: new Map,
|
|
3315
|
+
fileHashes: new Map
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function createIncrementalCompiler() {
|
|
3319
|
+
let previousSnapshot;
|
|
3320
|
+
let previousResult;
|
|
3321
|
+
const cache = createDependencyGraphCache();
|
|
3322
|
+
return {
|
|
3323
|
+
async compile(options, changedPaths) {
|
|
3324
|
+
const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
|
|
3325
|
+
const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
|
|
3326
|
+
const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && changedFiles.length === 0);
|
|
3327
|
+
if (cacheHit && previousResult) {
|
|
3328
|
+
return {
|
|
3329
|
+
...previousResult,
|
|
3330
|
+
stats: {
|
|
3331
|
+
cacheHit: true,
|
|
3332
|
+
changedFiles: [],
|
|
3333
|
+
affectedModules: [],
|
|
3334
|
+
reusedModules: previousResult.graph.modules.map((m) => m.name),
|
|
3335
|
+
reanalyzedModules: []
|
|
3336
|
+
}
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
if (previousResult && previousSnapshot && changedFiles.length > 0 && !await requiresGraphRebuild(options.rootDir, changedFiles)) {
|
|
3340
|
+
const stats2 = {
|
|
3341
|
+
cacheHit: true,
|
|
3342
|
+
changedFiles,
|
|
3343
|
+
affectedModules: findAffectedModules(previousResult.graph.modules, previousResult.graph.modules, changedFiles),
|
|
3344
|
+
reusedModules: previousResult.graph.modules.map((m) => m.name),
|
|
3345
|
+
reanalyzedModules: []
|
|
3346
|
+
};
|
|
3347
|
+
previousSnapshot = snapshot;
|
|
3348
|
+
return { ...previousResult, written: [], stats: stats2 };
|
|
3349
|
+
}
|
|
3350
|
+
const activeCache = options.cache ?? cache;
|
|
3351
|
+
if (!activeCache.dependencyGraph && previousResult) {
|
|
3352
|
+
activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
|
|
3353
|
+
}
|
|
3354
|
+
const result = await compileProject({ ...options, cache: activeCache });
|
|
3355
|
+
const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
|
|
3356
|
+
const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
|
|
3357
|
+
const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
|
|
3358
|
+
if (activeCache) {
|
|
3359
|
+
activeCache.dependencyGraph = new ModuleDependencyGraph(result.graph.modules);
|
|
3360
|
+
}
|
|
3361
|
+
const stats = {
|
|
3362
|
+
cacheHit: false,
|
|
3363
|
+
changedFiles,
|
|
3364
|
+
affectedModules,
|
|
3365
|
+
reusedModules,
|
|
3366
|
+
reanalyzedModules
|
|
3367
|
+
};
|
|
3368
|
+
previousSnapshot = snapshot;
|
|
3369
|
+
previousResult = result;
|
|
3370
|
+
return { ...result, stats };
|
|
3371
|
+
},
|
|
3372
|
+
reset() {
|
|
3373
|
+
previousSnapshot = undefined;
|
|
3374
|
+
previousResult = undefined;
|
|
3375
|
+
cache.modules.clear();
|
|
3376
|
+
cache.fileHashes.clear();
|
|
3377
|
+
cache.dependencyGraph = undefined;
|
|
3378
|
+
},
|
|
3379
|
+
getCache() {
|
|
3380
|
+
return cache;
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
async function updateSnapshot(previous, options, changedPaths) {
|
|
3385
|
+
const rootDir = resolve(options.rootDir);
|
|
3386
|
+
const outDir = resolve(options.outDir);
|
|
3387
|
+
const files = { ...previous.files };
|
|
3388
|
+
for (const changedPath of changedPaths) {
|
|
3389
|
+
const absolutePath = resolve(rootDir, changedPath);
|
|
3390
|
+
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
3391
|
+
continue;
|
|
3392
|
+
const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
|
|
3393
|
+
try {
|
|
3394
|
+
await access(absolutePath);
|
|
3395
|
+
const content = await readFile(absolutePath);
|
|
3396
|
+
files[relativePath] = createHash2("sha256").update(content).digest("hex");
|
|
3397
|
+
} catch {
|
|
3398
|
+
delete files[relativePath];
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
return { files, optionsKey: optionsKeyOf(options) };
|
|
3402
|
+
}
|
|
3403
|
+
async function createSnapshot(options) {
|
|
3404
|
+
const rootDir = resolve(options.rootDir);
|
|
3405
|
+
const outDir = resolve(options.outDir);
|
|
3406
|
+
const paths = await listSourceFiles(rootDir, outDir);
|
|
3407
|
+
const files = {};
|
|
3408
|
+
for (const path of paths) {
|
|
3409
|
+
const content = await readFile(path);
|
|
3410
|
+
files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
|
|
3411
|
+
}
|
|
3412
|
+
return { files, optionsKey: optionsKeyOf(options) };
|
|
3413
|
+
}
|
|
3414
|
+
function optionsKeyOf(options) {
|
|
3415
|
+
return JSON.stringify({
|
|
3416
|
+
include: options.include,
|
|
3417
|
+
strict: options.strict,
|
|
3418
|
+
moduleBoundaryPreset: options.moduleBoundaryPreset,
|
|
3419
|
+
moduleBoundaries: options.moduleBoundaries,
|
|
3420
|
+
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
3421
|
+
commandCapabilities: options.commandCapabilities,
|
|
3422
|
+
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
3423
|
+
detectOrphanModules: options.detectOrphanModules,
|
|
3424
|
+
generateClient: options.generateClient,
|
|
3425
|
+
generatePermissions: options.generatePermissions
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
async function requiresGraphRebuild(rootDir, changedFiles) {
|
|
3429
|
+
for (const relativePath of changedFiles) {
|
|
3430
|
+
const path = resolve(rootDir, relativePath);
|
|
3431
|
+
try {
|
|
3432
|
+
const source = await readFile(path, "utf8");
|
|
3433
|
+
if (/@(?:Module|Injectable|Inject|Controller|Command|Query)\b|new\s+InjectionToken\b|\bdefineModule\s*\(/.test(source))
|
|
3434
|
+
return true;
|
|
3435
|
+
} catch {
|
|
3436
|
+
return true;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return false;
|
|
3440
|
+
}
|
|
3441
|
+
async function listSourceFiles(rootDir, outDir) {
|
|
3442
|
+
const result = [];
|
|
3443
|
+
const visit = async (directory) => {
|
|
3444
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
3445
|
+
const path = resolve(directory, entry.name);
|
|
3446
|
+
if (entry.isDirectory()) {
|
|
3447
|
+
if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
|
|
3448
|
+
continue;
|
|
3449
|
+
await visit(path);
|
|
3450
|
+
} else if (/\.(tsx?|mts|cts)$/.test(entry.name)) {
|
|
3451
|
+
result.push(path);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
await visit(rootDir);
|
|
3456
|
+
return result.sort();
|
|
3457
|
+
}
|
|
3458
|
+
function diffFiles(previous, current) {
|
|
3459
|
+
if (!previous)
|
|
3460
|
+
return Object.keys(current);
|
|
3461
|
+
const names = new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
3462
|
+
return [...names].filter((name) => previous[name] !== current[name]).sort();
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
class ModuleDependencyGraph {
|
|
3466
|
+
imports = new Map;
|
|
3467
|
+
dependents = new Map;
|
|
3468
|
+
fileOwners = new Map;
|
|
3469
|
+
moduleMap = new Map;
|
|
3470
|
+
constructor(modules = []) {
|
|
3471
|
+
this.rebuild(modules);
|
|
3472
|
+
}
|
|
3473
|
+
rebuild(modules) {
|
|
3474
|
+
this.imports.clear();
|
|
3475
|
+
this.dependents.clear();
|
|
3476
|
+
this.fileOwners.clear();
|
|
3477
|
+
this.moduleMap.clear();
|
|
3478
|
+
for (const mod of modules) {
|
|
3479
|
+
this.moduleMap.set(mod.name, mod);
|
|
3480
|
+
this.imports.set(mod.name, new Set(mod.imports));
|
|
3481
|
+
if (!this.dependents.has(mod.name)) {
|
|
3482
|
+
this.dependents.set(mod.name, new Set);
|
|
3483
|
+
}
|
|
3484
|
+
this.indexFile(mod.file, mod.name);
|
|
3485
|
+
for (const p of mod.providers) {
|
|
3486
|
+
if (p.importPath)
|
|
3487
|
+
this.indexFile(p.importPath, mod.name);
|
|
3488
|
+
if (p.file)
|
|
3489
|
+
this.indexFile(p.file, mod.name);
|
|
3490
|
+
}
|
|
3491
|
+
for (const c of mod.controllers) {
|
|
3492
|
+
if (c.importPath)
|
|
3493
|
+
this.indexFile(c.importPath, mod.name);
|
|
3494
|
+
if (c.file)
|
|
3495
|
+
this.indexFile(c.file, mod.name);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
for (const [modName, imps] of this.imports.entries()) {
|
|
3499
|
+
for (const imp of imps) {
|
|
3500
|
+
if (!this.dependents.has(imp)) {
|
|
3501
|
+
this.dependents.set(imp, new Set);
|
|
3502
|
+
}
|
|
3503
|
+
this.dependents.get(imp).add(modName);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
indexFile(path, moduleName) {
|
|
3508
|
+
if (!path)
|
|
3509
|
+
return;
|
|
3510
|
+
const normalized = path.replace(/\.(tsx?|mts|cts)$/, "");
|
|
3511
|
+
if (!this.fileOwners.has(normalized)) {
|
|
3512
|
+
this.fileOwners.set(normalized, new Set);
|
|
3513
|
+
}
|
|
3514
|
+
this.fileOwners.get(normalized).add(moduleName);
|
|
3515
|
+
}
|
|
3516
|
+
getModulesOwningFile(filePath) {
|
|
3517
|
+
const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
|
|
3518
|
+
return Array.from(this.fileOwners.get(normalized) ?? []);
|
|
3519
|
+
}
|
|
3520
|
+
getAffectedModules(changedFiles) {
|
|
3521
|
+
if (changedFiles.length === 0)
|
|
3522
|
+
return [];
|
|
3523
|
+
const directlyAffected = new Set;
|
|
3524
|
+
for (const file of changedFiles) {
|
|
3525
|
+
for (const modName of this.getModulesOwningFile(file)) {
|
|
3526
|
+
directlyAffected.add(modName);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
if (directlyAffected.size === 0) {
|
|
3530
|
+
return Array.from(this.moduleMap.keys());
|
|
3531
|
+
}
|
|
3532
|
+
const affected = new Set(directlyAffected);
|
|
3533
|
+
const queue = Array.from(directlyAffected);
|
|
3534
|
+
while (queue.length > 0) {
|
|
3535
|
+
const current = queue.shift();
|
|
3536
|
+
const dependents = this.dependents.get(current);
|
|
3537
|
+
if (dependents) {
|
|
3538
|
+
for (const dep of dependents) {
|
|
3539
|
+
if (!affected.has(dep)) {
|
|
3540
|
+
affected.add(dep);
|
|
3541
|
+
queue.push(dep);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return Array.from(this.moduleMap.keys()).filter((name) => affected.has(name));
|
|
3547
|
+
}
|
|
3548
|
+
getDirectImports(moduleName) {
|
|
3549
|
+
return Array.from(this.imports.get(moduleName) ?? []);
|
|
3550
|
+
}
|
|
3551
|
+
getDirectDependents(moduleName) {
|
|
3552
|
+
return Array.from(this.dependents.get(moduleName) ?? []);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
function findAffectedModules(previous, current, changedFiles) {
|
|
3556
|
+
const depGraph = new ModuleDependencyGraph(current);
|
|
3557
|
+
return depGraph.getAffectedModules(changedFiles);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// src/watch.ts
|
|
3561
|
+
var DEFAULT_DEBOUNCE_MS = 100;
|
|
3562
|
+
function watchProject(options) {
|
|
3563
|
+
const rootDir = resolve2(options.rootDir);
|
|
3564
|
+
const outDir = resolve2(options.outDir);
|
|
3565
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
3566
|
+
let timer;
|
|
3567
|
+
let closed = false;
|
|
3568
|
+
let compiling = false;
|
|
3569
|
+
let pending = false;
|
|
3570
|
+
const pendingPaths = new Set;
|
|
3571
|
+
let watcher;
|
|
3572
|
+
const incremental = createIncrementalCompiler();
|
|
3573
|
+
let initialEvent;
|
|
3574
|
+
let resolveReady;
|
|
3575
|
+
let rejectReady;
|
|
3576
|
+
const ready = new Promise((resolvePromise, rejectPromise) => {
|
|
3577
|
+
resolveReady = resolvePromise;
|
|
3578
|
+
rejectReady = rejectPromise;
|
|
3579
|
+
});
|
|
3580
|
+
const emit = (event) => {
|
|
3581
|
+
options.onEvent?.(event);
|
|
3582
|
+
if (event.initial)
|
|
3583
|
+
initialEvent = event;
|
|
3584
|
+
};
|
|
3585
|
+
const compile = async (initial, changedPaths = []) => {
|
|
3586
|
+
if (closed && !initial)
|
|
3587
|
+
return;
|
|
3588
|
+
if (compiling) {
|
|
3589
|
+
pending = true;
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
compiling = true;
|
|
3593
|
+
const startedAt = performance.now();
|
|
3594
|
+
options.onEvent?.({
|
|
3595
|
+
type: "compile-start",
|
|
3596
|
+
initial,
|
|
3597
|
+
durationMs: 0,
|
|
3598
|
+
diagnostics: [],
|
|
3599
|
+
written: []
|
|
3600
|
+
});
|
|
3601
|
+
try {
|
|
3602
|
+
const result = await incremental.compile({ ...options, writeOnError: false }, changedPaths);
|
|
3603
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
3604
|
+
const hasErrors = result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
3605
|
+
emit({
|
|
3606
|
+
type: hasErrors ? "compile-error" : "compiled",
|
|
3607
|
+
initial,
|
|
3608
|
+
durationMs,
|
|
3609
|
+
diagnostics: result.diagnostics,
|
|
3610
|
+
written: result.written,
|
|
3611
|
+
stats: result.stats
|
|
3612
|
+
});
|
|
3613
|
+
} catch (error) {
|
|
3614
|
+
rejectReady(error);
|
|
3615
|
+
throw error;
|
|
3616
|
+
} finally {
|
|
3617
|
+
compiling = false;
|
|
3618
|
+
if (pending && !closed) {
|
|
3619
|
+
pending = false;
|
|
3620
|
+
compile(false, [...pendingPaths]);
|
|
3621
|
+
pendingPaths.clear();
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
const schedule = (changedPath) => {
|
|
3626
|
+
if (closed)
|
|
3627
|
+
return;
|
|
3628
|
+
if (changedPath)
|
|
3629
|
+
pendingPaths.add(changedPath);
|
|
3630
|
+
if (timer)
|
|
3631
|
+
clearTimeout(timer);
|
|
3632
|
+
timer = setTimeout(() => {
|
|
3633
|
+
timer = undefined;
|
|
3634
|
+
compile(false, [...pendingPaths]);
|
|
3635
|
+
pendingPaths.clear();
|
|
3636
|
+
}, debounceMs);
|
|
3637
|
+
};
|
|
3638
|
+
compile(true).then(() => {
|
|
3639
|
+
if (closed)
|
|
3640
|
+
return;
|
|
3641
|
+
watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
3642
|
+
if (!filename)
|
|
3643
|
+
return schedule();
|
|
3644
|
+
const changedPath = resolve2(rootDir, filename.toString());
|
|
3645
|
+
const relativePath = relative3(outDir, changedPath);
|
|
3646
|
+
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
3647
|
+
return;
|
|
3648
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
3649
|
+
schedule(relative3(rootDir, changedPath));
|
|
3650
|
+
});
|
|
3651
|
+
if (initialEvent)
|
|
3652
|
+
resolveReady(initialEvent);
|
|
3653
|
+
}).catch(() => {
|
|
3654
|
+
return;
|
|
3655
|
+
});
|
|
3656
|
+
return {
|
|
3657
|
+
ready,
|
|
3658
|
+
async close() {
|
|
3659
|
+
closed = true;
|
|
3660
|
+
if (timer)
|
|
3661
|
+
clearTimeout(timer);
|
|
3662
|
+
watcher?.close();
|
|
3663
|
+
await ready.catch(() => {
|
|
3664
|
+
return;
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
|
|
1590
3670
|
// src/cli.ts
|
|
1591
3671
|
function printUsage() {
|
|
1592
3672
|
console.log(`
|
|
@@ -1595,15 +3675,27 @@ function printUsage() {
|
|
|
1595
3675
|
Usage:
|
|
1596
3676
|
supacloud-compiler compile [rootDir] [options]
|
|
1597
3677
|
supacloud-compiler check [rootDir] [options]
|
|
3678
|
+
supacloud-compiler dev [rootDir] [options]
|
|
3679
|
+
supacloud-compiler graph [rootDir] [options]
|
|
3680
|
+
supacloud-compiler explain <name> [rootDir] [options]
|
|
3681
|
+
supacloud-compiler doctor [rootDir] [options]
|
|
1598
3682
|
|
|
1599
3683
|
Commands:
|
|
1600
3684
|
compile Compile application modules and generate artifacts
|
|
1601
3685
|
check Check artifact drift and run governance gates
|
|
3686
|
+
dev Watch source files and recompile on changes
|
|
3687
|
+
graph Print the discovered application graph
|
|
3688
|
+
explain Explain a module, provider, or external token
|
|
3689
|
+
doctor Run project and generated-artifact health checks
|
|
1602
3690
|
|
|
1603
3691
|
Options:
|
|
1604
3692
|
--root, -r <dir> Application source root (default: current directory or first positional argument)
|
|
1605
3693
|
--out, -o <dir> Artifact output directory (default: <rootDir>/generated)
|
|
1606
3694
|
--strict Treat all warnings as errors
|
|
3695
|
+
--client Generate typed API client in client.ts
|
|
3696
|
+
--permissions Generate typed permissions registry in permissions.ts
|
|
3697
|
+
--debounce <ms> Debounce source changes in dev mode (default: 100)
|
|
3698
|
+
--json Print machine-readable output for graph/explain/doctor
|
|
1607
3699
|
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
1608
3700
|
--help, -h Show this help
|
|
1609
3701
|
`);
|
|
@@ -1615,7 +3707,7 @@ async function run() {
|
|
|
1615
3707
|
process.exit(0);
|
|
1616
3708
|
}
|
|
1617
3709
|
const command = args[0];
|
|
1618
|
-
if (
|
|
3710
|
+
if (!["compile", "check", "dev", "graph", "explain", "doctor"].includes(command)) {
|
|
1619
3711
|
console.error(`Error: unknown command "${command}"`);
|
|
1620
3712
|
printUsage();
|
|
1621
3713
|
process.exit(1);
|
|
@@ -1623,7 +3715,12 @@ async function run() {
|
|
|
1623
3715
|
let rootDir = ".";
|
|
1624
3716
|
let outDir;
|
|
1625
3717
|
let strict = false;
|
|
3718
|
+
let generateClient = false;
|
|
3719
|
+
let generatePermissions = false;
|
|
1626
3720
|
let preset;
|
|
3721
|
+
let debounceMs = 100;
|
|
3722
|
+
let query;
|
|
3723
|
+
let json = false;
|
|
1627
3724
|
for (let i = 1;i < args.length; i++) {
|
|
1628
3725
|
const arg = args[i];
|
|
1629
3726
|
if (arg === "--root" || arg === "-r") {
|
|
@@ -1632,26 +3729,41 @@ async function run() {
|
|
|
1632
3729
|
outDir = args[++i];
|
|
1633
3730
|
} else if (arg === "--strict") {
|
|
1634
3731
|
strict = true;
|
|
3732
|
+
} else if (arg === "--client") {
|
|
3733
|
+
generateClient = true;
|
|
3734
|
+
} else if (arg === "--permissions") {
|
|
3735
|
+
generatePermissions = true;
|
|
3736
|
+
} else if (arg === "--debounce") {
|
|
3737
|
+
debounceMs = Number(args[++i]);
|
|
3738
|
+
if (!Number.isFinite(debounceMs) || debounceMs < 0) {
|
|
3739
|
+
console.error("Error: --debounce must be a non-negative number");
|
|
3740
|
+
process.exit(1);
|
|
3741
|
+
}
|
|
3742
|
+
} else if (arg === "--json") {
|
|
3743
|
+
json = true;
|
|
1635
3744
|
} else if (arg === "--preset" || arg === "-p") {
|
|
1636
3745
|
preset = args[++i];
|
|
1637
3746
|
} else if (!arg.startsWith("-") && rootDir === ".") {
|
|
1638
|
-
|
|
3747
|
+
if (command === "explain" && !query)
|
|
3748
|
+
query = arg;
|
|
3749
|
+
else
|
|
3750
|
+
rootDir = arg;
|
|
3751
|
+
} else if (!arg.startsWith("-") && command === "explain" && !query) {
|
|
3752
|
+
query = arg;
|
|
1639
3753
|
}
|
|
1640
3754
|
}
|
|
1641
|
-
const resolvedRoot =
|
|
1642
|
-
const resolvedOut = outDir ?
|
|
3755
|
+
const resolvedRoot = resolve3(process.cwd(), rootDir);
|
|
3756
|
+
const resolvedOut = outDir ? resolve3(process.cwd(), outDir) : resolve3(resolvedRoot, "generated");
|
|
1643
3757
|
if (command === "compile") {
|
|
1644
3758
|
const result = await compileProject({
|
|
1645
3759
|
rootDir: resolvedRoot,
|
|
1646
3760
|
outDir: resolvedOut,
|
|
1647
3761
|
strict,
|
|
1648
|
-
moduleBoundaryPreset: preset
|
|
3762
|
+
moduleBoundaryPreset: preset,
|
|
3763
|
+
generateClient,
|
|
3764
|
+
generatePermissions
|
|
1649
3765
|
});
|
|
1650
|
-
|
|
1651
|
-
const loc = diag.file ? ` ${diag.file}${diag.line ? `:${diag.line}` : ""}` : "";
|
|
1652
|
-
const log = diag.severity === "error" ? console.error : console.warn;
|
|
1653
|
-
log(`[${diag.severity}] ${diag.code}${loc}: ${diag.message}`);
|
|
1654
|
-
}
|
|
3766
|
+
printDiagnostics(result.diagnostics);
|
|
1655
3767
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1656
3768
|
if (errors.length > 0) {
|
|
1657
3769
|
console.error(`
|
|
@@ -1662,18 +3774,16 @@ Compilation failed with ${errors.length} error(s).`);
|
|
|
1662
3774
|
Compilation succeeded. Generated artifacts:
|
|
1663
3775
|
${result.written.map((f) => ` - ${f}`).join(`
|
|
1664
3776
|
`)}`);
|
|
1665
|
-
} else {
|
|
3777
|
+
} else if (command === "check") {
|
|
1666
3778
|
const result = await checkProject({
|
|
1667
3779
|
rootDir: resolvedRoot,
|
|
1668
3780
|
outDir: resolvedOut,
|
|
1669
3781
|
strict,
|
|
1670
|
-
moduleBoundaryPreset: preset
|
|
3782
|
+
moduleBoundaryPreset: preset,
|
|
3783
|
+
generateClient,
|
|
3784
|
+
generatePermissions
|
|
1671
3785
|
});
|
|
1672
|
-
|
|
1673
|
-
const loc = diag.file ? ` ${diag.file}${diag.line ? `:${diag.line}` : ""}` : "";
|
|
1674
|
-
const log = diag.severity === "error" ? console.error : console.warn;
|
|
1675
|
-
log(`[${diag.severity}] ${diag.code}${loc}: ${diag.message}`);
|
|
1676
|
-
}
|
|
3786
|
+
printDiagnostics(result.diagnostics);
|
|
1677
3787
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1678
3788
|
if (errors.length > 0) {
|
|
1679
3789
|
console.error(`
|
|
@@ -1690,6 +3800,91 @@ Artifact drift detected:`);
|
|
|
1690
3800
|
process.exit(1);
|
|
1691
3801
|
}
|
|
1692
3802
|
console.log("Artifact check passed: disk files match compiler output with no drift.");
|
|
3803
|
+
} else if (command === "dev") {
|
|
3804
|
+
const handle = watchProject({
|
|
3805
|
+
rootDir: resolvedRoot,
|
|
3806
|
+
outDir: resolvedOut,
|
|
3807
|
+
strict,
|
|
3808
|
+
moduleBoundaryPreset: preset,
|
|
3809
|
+
debounceMs,
|
|
3810
|
+
generateClient,
|
|
3811
|
+
generatePermissions,
|
|
3812
|
+
onEvent: (event) => {
|
|
3813
|
+
if (event.type === "compile-start") {
|
|
3814
|
+
console.log(event.initial ? `
|
|
3815
|
+
Initial compilation...` : `
|
|
3816
|
+
Source change detected; compiling...`);
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
printDiagnostics(event.diagnostics);
|
|
3820
|
+
if (event.type === "compile-error") {
|
|
3821
|
+
console.error(`Compilation failed; keeping the last successful artifacts (${event.durationMs}ms).`);
|
|
3822
|
+
} else {
|
|
3823
|
+
const cache = event.stats?.cacheHit ? "cache hit" : "recompiled";
|
|
3824
|
+
const affected = event.stats?.affectedModules?.length ? `; affected modules: ${event.stats.affectedModules.join(", ")}` : "";
|
|
3825
|
+
console.log(`Compilation succeeded in ${event.durationMs}ms (${cache}${affected}).`);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
const close = async () => {
|
|
3830
|
+
await handle.close();
|
|
3831
|
+
process.exit(0);
|
|
3832
|
+
};
|
|
3833
|
+
process.once("SIGINT", () => void close());
|
|
3834
|
+
process.once("SIGTERM", () => void close());
|
|
3835
|
+
await handle.ready;
|
|
3836
|
+
await new Promise(() => {
|
|
3837
|
+
return;
|
|
3838
|
+
});
|
|
3839
|
+
} else if (command === "graph") {
|
|
3840
|
+
const graph = await analyzeProject(resolvedRoot);
|
|
3841
|
+
if (json)
|
|
3842
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
3843
|
+
else
|
|
3844
|
+
console.log(formatGraph(graph));
|
|
3845
|
+
} else if (command === "explain") {
|
|
3846
|
+
if (!query) {
|
|
3847
|
+
console.error("Error: explain requires a module, provider, or external token name");
|
|
3848
|
+
process.exit(1);
|
|
3849
|
+
}
|
|
3850
|
+
try {
|
|
3851
|
+
const graph = await analyzeProject(resolvedRoot);
|
|
3852
|
+
const explanation = explainGraph(graph, query);
|
|
3853
|
+
if (json)
|
|
3854
|
+
console.log(JSON.stringify({ subject: query, explanation }, null, 2));
|
|
3855
|
+
else
|
|
3856
|
+
console.log(explanation);
|
|
3857
|
+
} catch (error) {
|
|
3858
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3859
|
+
process.exit(1);
|
|
3860
|
+
}
|
|
3861
|
+
} else {
|
|
3862
|
+
const result = await checkProject({
|
|
3863
|
+
rootDir: resolvedRoot,
|
|
3864
|
+
outDir: resolvedOut,
|
|
3865
|
+
strict,
|
|
3866
|
+
moduleBoundaryPreset: preset
|
|
3867
|
+
});
|
|
3868
|
+
const doctor = doctorProject(resolvedRoot, resolvedOut, result.graph, result.upToDate, result.diagnostics);
|
|
3869
|
+
if (json) {
|
|
3870
|
+
console.log(JSON.stringify(doctor, null, 2));
|
|
3871
|
+
} else {
|
|
3872
|
+
for (const check of doctor.checks)
|
|
3873
|
+
console.log(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
3874
|
+
printDiagnostics(doctor.diagnostics);
|
|
3875
|
+
}
|
|
3876
|
+
if (doctor.errors > 0)
|
|
3877
|
+
process.exit(1);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
function printDiagnostics(diagnostics) {
|
|
3881
|
+
for (const diag of diagnostics) {
|
|
3882
|
+
const loc = diag.file ? ` ${diag.file}${diag.line ? `:${diag.line}` : ""}` : "";
|
|
3883
|
+
const log = diag.severity === "error" ? console.error : console.warn;
|
|
3884
|
+
log(`[${diag.severity}] ${diag.code}${loc}: ${diag.message}`);
|
|
3885
|
+
if (diag.suggestion) {
|
|
3886
|
+
console.log(` Hint: ${diag.suggestion}`);
|
|
3887
|
+
}
|
|
1693
3888
|
}
|
|
1694
3889
|
}
|
|
1695
3890
|
run().catch((err) => {
|