@supacloud/compiler 0.4.1 → 0.6.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/dist/index.js CHANGED
@@ -1,11 +1,330 @@
1
1
  // src/analyze.ts
2
- import { existsSync } from "node:fs";
3
- import { join, relative, sep } from "node:path";
4
- import {
5
- Node,
6
- Project,
7
- SyntaxKind
8
- } from "ts-morph";
2
+ import { createHash as createHash3 } from "node:crypto";
3
+ import { relative, resolve as resolvePath, sep } from "node:path";
4
+ import * as ts3 from "@typescript/typescript6";
5
+
6
+ // src/program.ts
7
+ import { createHash as createHash2 } from "node:crypto";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import * as ts2 from "@typescript/typescript6";
11
+
12
+ // src/traits.ts
13
+ import { createHash } from "node:crypto";
14
+ import * as ts from "@typescript/typescript6";
15
+ class TraitCompiler {
16
+ handlers;
17
+ constructor(handlers = createDefaultTraitHandlers()) {
18
+ this.handlers = handlers;
19
+ }
20
+ compile(program, previous, changedFiles) {
21
+ const byFile = new Map;
22
+ for (const sourceFile of program.getSourceFiles()) {
23
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("/node_modules/"))
24
+ continue;
25
+ const previousTraits = previous?.byFile.get(sourceFile.fileName);
26
+ if (previousTraits && !changedFiles.has(sourceFile.fileName)) {
27
+ byFile.set(sourceFile.fileName, previousTraits);
28
+ continue;
29
+ }
30
+ byFile.set(sourceFile.fileName, this.compileSourceFile(sourceFile));
31
+ }
32
+ const all = [...byFile.values()].flat().sort((a, b) => a.file.localeCompare(b.file) || a.start - b.start || a.kind.localeCompare(b.kind));
33
+ return { byFile, all };
34
+ }
35
+ compileSourceFile(sourceFile) {
36
+ const traits = [];
37
+ const visit = (node) => {
38
+ for (const handler of this.handlers) {
39
+ const name = handler.detect(node);
40
+ if (name)
41
+ traits.push(record(handler.kind, name, sourceFile, node));
42
+ }
43
+ ts.forEachChild(node, visit);
44
+ };
45
+ visit(sourceFile);
46
+ return traits;
47
+ }
48
+ }
49
+ function compileTraits(program, previous, changedFiles) {
50
+ return new TraitCompiler().compile(program, previous, changedFiles);
51
+ }
52
+ function record(kind, name, sourceFile, node) {
53
+ const text = node.getText(sourceFile);
54
+ return {
55
+ kind,
56
+ name,
57
+ file: sourceFile.fileName,
58
+ start: node.getStart(sourceFile),
59
+ end: node.end,
60
+ fingerprint: createHash("sha1").update(`${kind}:${text}`).digest("hex")
61
+ };
62
+ }
63
+ function decoratorName(decorator) {
64
+ return expressionName(ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression);
65
+ }
66
+ function expressionName(expression) {
67
+ if (ts.isIdentifier(expression))
68
+ return expression.text;
69
+ if (ts.isPropertyAccessExpression(expression))
70
+ return expression.name.text;
71
+ return "";
72
+ }
73
+ function createDefaultTraitHandlers() {
74
+ return [
75
+ {
76
+ kind: "module",
77
+ detect: decoratedDeclaration("Module")
78
+ },
79
+ {
80
+ kind: "injectable",
81
+ detect: decoratedDeclaration("Injectable")
82
+ },
83
+ {
84
+ kind: "controller",
85
+ detect: decoratedDeclaration("Controller")
86
+ },
87
+ {
88
+ kind: "command",
89
+ detect: decoratedDeclaration("Command")
90
+ },
91
+ {
92
+ kind: "query",
93
+ detect: decoratedDeclaration("Query")
94
+ },
95
+ {
96
+ kind: "defineModule",
97
+ detect: (node) => {
98
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
99
+ return;
100
+ const initializer = node.initializer;
101
+ return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineModule" ? node.name.text : undefined;
102
+ }
103
+ },
104
+ {
105
+ kind: "injectionToken",
106
+ detect: (node) => {
107
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
108
+ return;
109
+ const initializer = node.initializer;
110
+ return initializer && ts.isNewExpression(initializer) && expressionName(initializer.expression) === "InjectionToken" ? node.name.text : undefined;
111
+ }
112
+ }
113
+ ];
114
+ }
115
+ function decoratedDeclaration(decorator) {
116
+ return (node) => {
117
+ if (!ts.isClassDeclaration(node) || !node.name)
118
+ return;
119
+ return (ts.getDecorators(node) ?? []).some((item) => decoratorName(item) === decorator) ? node.name.text : undefined;
120
+ };
121
+ }
122
+
123
+ // src/program.ts
124
+ function createIncrementalProgramSession(projectRoot) {
125
+ const rootDir = resolve(projectRoot);
126
+ let projectConfig = readProjectConfig(rootDir);
127
+ let projectConfigKey = configKey(projectConfig);
128
+ let builder;
129
+ let traits;
130
+ const sourceFileCache = new Map;
131
+ return {
132
+ getProgram() {
133
+ if (!builder) {
134
+ throw new Error("incremental TypeScript program has not been initialized");
135
+ }
136
+ return builder.getProgram();
137
+ },
138
+ getTypeChecker() {
139
+ return this.getProgram().getTypeChecker();
140
+ },
141
+ update(rootNames, changedPaths = rootNames) {
142
+ const oldProgram = builder?.getProgram();
143
+ const oldSourceFiles = new Map(oldProgram?.getSourceFiles().map((sourceFile) => [canonical(sourceFile.fileName), sourceFile]) ?? []);
144
+ const nextProjectConfig = readProjectConfig(rootDir);
145
+ const nextProjectConfigKey = configKey(nextProjectConfig);
146
+ const configChanged = nextProjectConfigKey !== projectConfigKey;
147
+ const previousBuilder = configChanged ? undefined : builder;
148
+ if (configChanged) {
149
+ sourceFileCache.clear();
150
+ traits = undefined;
151
+ }
152
+ projectConfig = nextProjectConfig;
153
+ projectConfigKey = nextProjectConfigKey;
154
+ const normalizedRoots = [...new Set(rootNames.map((file) => resolve(rootDir, file)))].sort();
155
+ const normalizedChanged = [...new Set(changedPaths.map((file) => resolve(rootDir, file)))];
156
+ const invalidatedPaths = new Set(normalizedChanged.map(canonical));
157
+ for (const sourceFile of oldSourceFiles.values()) {
158
+ if (sourceVersion(sourceFile.fileName) !== sourceFileVersion(sourceFile)) {
159
+ invalidatedPaths.add(canonical(sourceFile.fileName));
160
+ }
161
+ }
162
+ const invalidateAllResolutions = configChanged || [...invalidatedPaths].some((fileName) => {
163
+ const wasInProgram = oldSourceFiles.has(fileName);
164
+ return wasInProgram !== existsSync(fileName);
165
+ });
166
+ for (const fileName of normalizedChanged) {
167
+ if (!existsSync(fileName))
168
+ sourceFileCache.delete(canonical(fileName));
169
+ }
170
+ const host = createHost(projectConfig.options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions);
171
+ builder = ts2.createEmitAndSemanticDiagnosticsBuilderProgram(normalizedRoots, projectConfig.options, host, previousBuilder, projectConfig.errors, projectConfig.projectReferences);
172
+ const program = builder.getProgram();
173
+ const changedFiles = [];
174
+ const reusedFiles = [];
175
+ const currentPaths = new Set(program.getSourceFiles().map((file) => canonical(file.fileName)));
176
+ for (const sourceFile of program.getSourceFiles()) {
177
+ if (sourceFile.isDeclarationFile)
178
+ continue;
179
+ const previous = oldSourceFiles.get(canonical(sourceFile.fileName));
180
+ if (previous && previous === sourceFile) {
181
+ reusedFiles.push(sourceFile.fileName);
182
+ } else {
183
+ changedFiles.push(sourceFile.fileName);
184
+ }
185
+ }
186
+ for (const [path, sourceFile] of oldSourceFiles) {
187
+ if (!sourceFile.isDeclarationFile && !currentPaths.has(path)) {
188
+ changedFiles.push(sourceFile.fileName);
189
+ }
190
+ }
191
+ for (const path of sourceFileCache.keys()) {
192
+ if (!currentPaths.has(path))
193
+ sourceFileCache.delete(path);
194
+ }
195
+ traits = compileTraits(program, traits, new Set(changedFiles));
196
+ return { changedFiles, reusedFiles, program };
197
+ },
198
+ getTraits() {
199
+ return traits?.all ?? [];
200
+ },
201
+ getDiagnostics() {
202
+ if (!builder)
203
+ return projectConfig.errors;
204
+ const program = builder.getProgram();
205
+ return [
206
+ ...projectConfig.errors,
207
+ ...program.getSyntacticDiagnostics()
208
+ ];
209
+ },
210
+ emit() {
211
+ if (!builder) {
212
+ throw new Error("incremental TypeScript program has not been initialized");
213
+ }
214
+ return builder.emit();
215
+ },
216
+ reset() {
217
+ builder = undefined;
218
+ projectConfig = readProjectConfig(rootDir);
219
+ projectConfigKey = configKey(projectConfig);
220
+ traits = undefined;
221
+ sourceFileCache.clear();
222
+ }
223
+ };
224
+ }
225
+ function createHost(options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions) {
226
+ const host = ts2.createIncrementalCompilerHost(options, {
227
+ ...ts2.sys,
228
+ getCurrentDirectory: () => rootDir
229
+ });
230
+ host.hasInvalidatedResolutions = (filePath) => invalidateAllResolutions || invalidatedPaths.has(canonical(filePath));
231
+ const originalGetSourceFile = host.getSourceFile.bind(host);
232
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
233
+ const key = canonical(fileName);
234
+ const text = host.readFile(fileName);
235
+ if (text === undefined) {
236
+ sourceFileCache.delete(key);
237
+ return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
238
+ }
239
+ const version = hashText(text);
240
+ const parseKey = sourceFileParseKey(languageVersion);
241
+ const cached = sourceFileCache.get(key);
242
+ if (!shouldCreateNewSourceFile && cached?.version === version && cached.parseKey === parseKey) {
243
+ return cached.sourceFile;
244
+ }
245
+ const sourceFile = originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
246
+ if (sourceFile) {
247
+ sourceFileCache.set(key, { sourceFile, version: hashText(sourceFile.text), parseKey });
248
+ } else {
249
+ sourceFileCache.delete(key);
250
+ }
251
+ return sourceFile;
252
+ };
253
+ return host;
254
+ }
255
+ function canonical(fileName) {
256
+ const normalized = resolve(fileName);
257
+ return ts2.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();
258
+ }
259
+ function sourceFileParseKey(languageVersion) {
260
+ return typeof languageVersion === "number" ? `target:${languageVersion}` : JSON.stringify({
261
+ languageVersion: languageVersion.languageVersion,
262
+ impliedNodeFormat: languageVersion.impliedNodeFormat,
263
+ jsDocParsingMode: languageVersion.jsDocParsingMode
264
+ });
265
+ }
266
+ function configKey(config) {
267
+ return JSON.stringify({
268
+ options: config.options,
269
+ projectReferences: config.projectReferences,
270
+ configFingerprint: config.configFingerprint
271
+ });
272
+ }
273
+ function hashText(text) {
274
+ return createHash2("sha1").update(text).digest("hex");
275
+ }
276
+ function sourceVersion(fileName) {
277
+ try {
278
+ return hashText(readFileSync(fileName, "utf8"));
279
+ } catch {
280
+ return "missing";
281
+ }
282
+ }
283
+ function sourceFileVersion(sourceFile) {
284
+ const descriptor = Object.getOwnPropertyDescriptor(sourceFile, "version");
285
+ return typeof descriptor?.value === "string" ? descriptor.value : undefined;
286
+ }
287
+ function readProjectConfig(rootDir) {
288
+ const configPath = join(rootDir, "tsconfig.json");
289
+ if (!existsSync(configPath)) {
290
+ return {
291
+ options: {
292
+ target: ts2.ScriptTarget.ES2022,
293
+ module: ts2.ModuleKind.ESNext,
294
+ moduleResolution: ts2.ModuleResolutionKind.Bundler,
295
+ experimentalDecorators: true,
296
+ allowJs: false,
297
+ skipLibCheck: true
298
+ },
299
+ errors: [],
300
+ projectReferences: undefined,
301
+ configFingerprint: "defaults"
302
+ };
303
+ }
304
+ const configReads = new Map;
305
+ const readConfig = (fileName) => {
306
+ const text = ts2.sys.readFile(fileName);
307
+ configReads.set(canonical(fileName), text === undefined ? "missing" : hashText(text));
308
+ return text;
309
+ };
310
+ const config = ts2.readConfigFile(configPath, readConfig);
311
+ if (config.error) {
312
+ return {
313
+ options: {},
314
+ errors: [config.error],
315
+ configFingerprint: JSON.stringify([...configReads])
316
+ };
317
+ }
318
+ const parsed = ts2.parseJsonConfigFileContent(config.config, { ...ts2.sys, readFile: readConfig }, dirname(configPath));
319
+ return {
320
+ options: parsed.options,
321
+ errors: parsed.errors,
322
+ projectReferences: parsed.projectReferences,
323
+ configFingerprint: JSON.stringify([...configReads])
324
+ };
325
+ }
326
+
327
+ // src/analyze.ts
9
328
  var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
10
329
  var ROUTE_DECORATORS = {
11
330
  Get: "GET",
@@ -17,60 +336,321 @@ var ROUTE_DECORATORS = {
17
336
  Options: "OPTIONS"
18
337
  };
19
338
  var SCOPES = ["application", "request", "job"];
20
- async function analyzeProject(rootDir, include) {
21
- const project = createProject(rootDir);
22
- const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
23
- project.addSourceFilesAtPaths(patterns);
24
- const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
339
+ function isScope(value) {
340
+ return SCOPES.some((scope) => scope === value);
341
+ }
342
+ function nodeText(node) {
343
+ return node.getText(node.getSourceFile());
344
+ }
345
+ function lineOf(node) {
346
+ const sourceFile = node.getSourceFile();
347
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
348
+ }
349
+ function variableName(decl) {
350
+ return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
351
+ }
352
+ function propertyName(name) {
353
+ if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
354
+ return name.text;
355
+ if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
356
+ return name.text;
357
+ return nodeText(name);
358
+ }
359
+ function parameterName(param) {
360
+ return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
361
+ }
362
+ function decoratorsOf(node) {
363
+ return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
364
+ }
365
+ function decoratorArguments(dec) {
366
+ return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
367
+ }
368
+ function hasMethod(cls, name) {
369
+ return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
370
+ }
371
+ function hasDestroyHook(cls) {
372
+ return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
373
+ }
374
+ function descendantsOfKind(root, predicate) {
375
+ const result = [];
376
+ const visit = (node) => {
377
+ if (predicate(node))
378
+ result.push(node);
379
+ ts3.forEachChild(node, visit);
380
+ };
381
+ visit(root);
382
+ return result;
383
+ }
384
+ async function analyzeProject(rootDir, include, cache, changedPaths) {
385
+ const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
386
+ if (cache)
387
+ cache.programSession = session;
388
+ const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
389
+ const update = session.update(rootNames, changedPaths);
390
+ const program = update.program;
391
+ const checker = program.getTypeChecker();
392
+ const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("/node_modules/") && !sf.fileName.includes("/dist/") && isProjectSourceFile(sf, rootDir)).sort((a, b) => a.fileName.localeCompare(b.fileName));
25
393
  const ctx = {
26
394
  rootDir,
395
+ program,
396
+ checker,
27
397
  tokensByName: new Map,
28
398
  classesByName: new Map,
29
399
  diagnostics: []
30
400
  };
401
+ const nativeTraitFiles = new Map;
402
+ for (const diagnostic of session.getDiagnostics()) {
403
+ ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
404
+ }
405
+ for (const trait of session.getTraits()) {
406
+ const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
407
+ kinds.add(trait.kind);
408
+ nativeTraitFiles.set(trait.file, kinds);
409
+ }
31
410
  for (const sf of sourceFiles) {
32
411
  indexFile(sf, ctx);
33
412
  }
34
413
  const candidates = [];
35
414
  for (const sf of sourceFiles) {
36
- for (const cls of sf.getClasses()) {
37
- const moduleDec = findDecorator(cls, "Module");
38
- if (!moduleDec)
39
- continue;
40
- const options = decoratorObjectArg(moduleDec);
41
- if (!options)
42
- continue;
43
- candidates.push({
44
- node: cls,
45
- options,
46
- className: cls.getName() ?? "<anonymous>",
47
- file: sf.getFilePath(),
48
- line: cls.getStartLineNumber()
49
- });
415
+ const traits = nativeTraitFiles.get(sf.fileName);
416
+ if (!cache || traits?.has("module")) {
417
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
418
+ const moduleDec = findDecorator(cls, "Module");
419
+ if (!moduleDec)
420
+ continue;
421
+ const options = decoratorObjectArg(moduleDec);
422
+ if (!options)
423
+ continue;
424
+ candidates.push({
425
+ node: cls,
426
+ options,
427
+ className: cls.name?.text ?? "<anonymous>",
428
+ file: sf.fileName,
429
+ line: lineOf(cls)
430
+ });
431
+ }
50
432
  }
51
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
52
- if (call.getExpression().getText() !== "defineModule")
53
- continue;
54
- const parent = call.getParent();
55
- if (!parent || !Node.isVariableDeclaration(parent))
56
- continue;
57
- const arg = call.getArguments()[0];
58
- if (!arg || !Node.isObjectLiteralExpression(arg))
59
- continue;
60
- candidates.push({
61
- node: parent,
62
- options: arg,
63
- className: parent.getName(),
64
- file: sf.getFilePath(),
65
- line: parent.getStartLineNumber()
66
- });
433
+ if (!cache || traits?.has("defineModule")) {
434
+ for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
435
+ if (nodeText(call.expression) !== "defineModule")
436
+ continue;
437
+ const parent = call.parent;
438
+ if (!parent || !ts3.isVariableDeclaration(parent))
439
+ continue;
440
+ const arg = call.arguments[0];
441
+ if (!arg || !ts3.isObjectLiteralExpression(arg))
442
+ continue;
443
+ candidates.push({
444
+ node: parent,
445
+ options: arg,
446
+ className: variableName(parent),
447
+ file: sf.fileName,
448
+ line: lineOf(parent)
449
+ });
450
+ }
67
451
  }
68
452
  }
69
453
  const nameByNode = new Map;
70
454
  for (const c of candidates) {
71
455
  nameByNode.set(c.node, stringLiteralProp(c.options, "name") ?? c.className);
72
456
  }
73
- const modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
457
+ let modules = [];
458
+ let reusedModules = [];
459
+ let reanalyzedModules = [];
460
+ if (cache) {
461
+ const currentFileHashes = new Map;
462
+ for (const sf of sourceFiles) {
463
+ const rel = sourcePath(rootDir, sf.fileName);
464
+ const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
465
+ currentFileHashes.set(rel, hash);
466
+ }
467
+ const changedFiles = new Set;
468
+ for (const [file, hash] of currentFileHashes.entries()) {
469
+ if (cache.fileHashes.get(file) !== hash) {
470
+ changedFiles.add(file);
471
+ }
472
+ }
473
+ for (const file of cache.fileHashes.keys()) {
474
+ if (!currentFileHashes.has(file)) {
475
+ changedFiles.add(file);
476
+ }
477
+ }
478
+ const modulesToKeep = new Map;
479
+ const finalModules = [];
480
+ const finalDiagnostics = [...ctx.diagnostics];
481
+ const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
482
+ for (const [modName, entry] of cache.modules.entries()) {
483
+ const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
484
+ const moduleFileExists = currentFileHashes.has(entry.module.file);
485
+ const isAffectedByDep = affectedModuleNames.has(modName);
486
+ if (!hasChangedFile && !isAffectedByDep && moduleFileExists) {
487
+ modulesToKeep.set(modName, entry);
488
+ reusedModules.push(modName);
489
+ finalModules.push(entry.module);
490
+ if (entry.diagnostics)
491
+ finalDiagnostics.push(...entry.diagnostics);
492
+ }
493
+ }
494
+ for (const c of candidates) {
495
+ const modName = nameByNode.get(c.node) ?? c.className;
496
+ if (modulesToKeep.has(modName)) {
497
+ continue;
498
+ }
499
+ const diagBefore = ctx.diagnostics.length;
500
+ const parsed = parseModule(c, nameByNode, ctx);
501
+ const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
502
+ const ownedFiles = collectModuleSourceClosure(parsed, ctx);
503
+ const fileHashes = {};
504
+ for (const f of ownedFiles) {
505
+ fileHashes[f] = currentFileHashes.get(f) ?? "";
506
+ }
507
+ cache.modules.set(parsed.name, {
508
+ module: parsed,
509
+ ownedFiles: [...ownedFiles],
510
+ fileHashes,
511
+ diagnostics: moduleDiagnostics
512
+ });
513
+ reanalyzedModules.push(parsed.name);
514
+ finalModules.push(parsed);
515
+ finalDiagnostics.push(...moduleDiagnostics);
516
+ }
517
+ for (const modName of [...cache.modules.keys()]) {
518
+ if (!modulesToKeep.has(modName) && !reanalyzedModules.includes(modName)) {
519
+ cache.modules.delete(modName);
520
+ }
521
+ }
522
+ cache.fileHashes = currentFileHashes;
523
+ cache.lastStats = { reusedModules, reanalyzedModules };
524
+ ctx.diagnostics = finalDiagnostics;
525
+ modules = finalModules;
526
+ } else {
527
+ modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
528
+ }
529
+ modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
530
+ const allRegisteredClasses = new Set;
531
+ const allRegisteredControllers = new Set;
532
+ const allRegisteredCommands = new Set;
533
+ for (const m of modules) {
534
+ for (const p of m.providers) {
535
+ if (p.useClass)
536
+ allRegisteredClasses.add(p.useClass);
537
+ if (p.kind === "class")
538
+ allRegisteredClasses.add(p.token);
539
+ }
540
+ for (const c of m.controllers) {
541
+ allRegisteredControllers.add(c.className);
542
+ }
543
+ for (const cmd of m.commands) {
544
+ allRegisteredCommands.add(cmd.className);
545
+ }
546
+ }
547
+ const rootProviders = [];
548
+ const standaloneControllers = [];
549
+ const standaloneCommands = [];
550
+ for (const [name, classInfo] of ctx.classesByName.entries()) {
551
+ if (!allRegisteredClasses.has(name)) {
552
+ const injectable = parseInjectableOptions(classInfo.decl, ctx);
553
+ if (injectable?.providedIn === "root") {
554
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
555
+ const file = sourcePath(ctx.rootDir, classInfo.file);
556
+ const line = lineOf(classInfo.decl);
557
+ if (missing) {
558
+ warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
559
+ }
560
+ rootProviders.push({
561
+ token: name,
562
+ tokenKind: "class",
563
+ kind: "class",
564
+ useClass: name,
565
+ scope: injectable.scope ?? "application",
566
+ deps,
567
+ optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
568
+ selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
569
+ skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
570
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
571
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
572
+ providedIn: "root",
573
+ hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
574
+ exported: true,
575
+ file,
576
+ line,
577
+ importPath: modulePath(ctx.rootDir, classInfo.file)
578
+ });
579
+ }
580
+ }
581
+ if (!allRegisteredControllers.has(name)) {
582
+ const controllerDec = findDecorator(classInfo.decl, "Controller");
583
+ if (controllerDec) {
584
+ const arg = decoratorArguments(controllerDec)[0];
585
+ const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
586
+ if (isStandalone) {
587
+ const ctrl = parseController(classInfo.decl, ctx);
588
+ if (ctrl)
589
+ standaloneControllers.push(ctrl);
590
+ }
591
+ }
592
+ }
593
+ if (!allRegisteredCommands.has(name)) {
594
+ const commandDec = findDecorator(classInfo.decl, "Command");
595
+ if (commandDec) {
596
+ const meta = decoratorObjectArg(commandDec);
597
+ if (meta && booleanProp(meta, "standalone")) {
598
+ standaloneCommands.push({
599
+ className: classInfo.decl.name?.text ?? name,
600
+ name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
601
+ permission: stringLiteralProp(meta, "permission"),
602
+ transaction: commandModeProp(meta, "transaction") ?? "none",
603
+ audit: stringLiteralProp(meta, "audit"),
604
+ idempotency: commandModeProp(meta, "idempotency") ?? "none",
605
+ standalone: true,
606
+ aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
607
+ });
608
+ }
609
+ }
610
+ }
611
+ }
612
+ for (const [name, tokenInfo] of ctx.tokensByName.entries()) {
613
+ if (tokenInfo.providedIn === "root" && !allRegisteredClasses.has(name)) {
614
+ rootProviders.push({
615
+ token: name,
616
+ tokenKind: "injection-token",
617
+ kind: "factory",
618
+ scope: tokenInfo.scope ?? "application",
619
+ deps: [],
620
+ providedIn: "root",
621
+ exported: true,
622
+ file: sourcePath(ctx.rootDir, tokenInfo.file),
623
+ line: tokenInfo.line ?? 1,
624
+ importPath: modulePath(ctx.rootDir, tokenInfo.file)
625
+ });
626
+ }
627
+ }
628
+ if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
629
+ const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
630
+ if (existingRoot) {
631
+ modules = modules.map((module) => module === existingRoot ? {
632
+ ...module,
633
+ providers: [...module.providers, ...rootProviders],
634
+ controllers: [...module.controllers, ...standaloneControllers],
635
+ commands: [...module.commands, ...standaloneCommands],
636
+ exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
637
+ } : module);
638
+ } else {
639
+ const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
640
+ modules.unshift({
641
+ name: "root",
642
+ className: "RootModule",
643
+ file: fallbackFile,
644
+ line: 1,
645
+ imports: [],
646
+ providers: rootProviders,
647
+ controllers: standaloneControllers,
648
+ commands: standaloneCommands,
649
+ queries: [],
650
+ exports: rootProviders.map((p) => p.token)
651
+ });
652
+ }
653
+ }
74
654
  const providedTokens = new Set(modules.flatMap((m) => m.providers.map((p) => p.token)));
75
655
  const referenced = new Set;
76
656
  for (const m of modules) {
@@ -89,28 +669,76 @@ async function analyzeProject(rootDir, include) {
89
669
  modules,
90
670
  externalTokens,
91
671
  diagnostics: ctx.diagnostics,
92
- tokenNames
672
+ tokenNames,
673
+ cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
93
674
  };
94
675
  }
95
- function createProject(rootDir) {
96
- const tsConfigFilePath = join(rootDir, "tsconfig.json");
97
- if (existsSync(tsConfigFilePath)) {
98
- return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
676
+ function collectModuleSourceClosure(module, ctx) {
677
+ const seeds = new Set;
678
+ const addRelativeModule = (path) => {
679
+ if (!path)
680
+ return;
681
+ const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
682
+ seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
683
+ };
684
+ addRelativeModule(module.file);
685
+ for (const provider of module.providers)
686
+ addRelativeModule(provider.importPath);
687
+ for (const controller of module.controllers)
688
+ addRelativeModule(controller.importPath);
689
+ const ownedFiles = new Set;
690
+ const queue = [...seeds];
691
+ while (queue.length > 0) {
692
+ const fileName = queue.shift();
693
+ if (!fileName)
694
+ continue;
695
+ const sourceFile = ctx.program.getSourceFile(fileName);
696
+ if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
697
+ continue;
698
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
699
+ if (ownedFiles.has(relativeFile))
700
+ continue;
701
+ ownedFiles.add(relativeFile);
702
+ for (const statement of sourceFile.statements) {
703
+ let moduleName;
704
+ if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
705
+ moduleName = statement.moduleSpecifier.text;
706
+ } else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
707
+ moduleName = statement.moduleSpecifier.text;
708
+ } else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
709
+ moduleName = statement.moduleReference.expression.text;
710
+ }
711
+ if (!moduleName || moduleName.startsWith("node:"))
712
+ continue;
713
+ const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
714
+ if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
715
+ queue.push(resolved);
716
+ }
99
717
  }
100
- return new Project({
101
- compilerOptions: { experimentalDecorators: true, allowJs: false }
102
- });
718
+ return ownedFiles;
719
+ }
720
+ function resolveSourcePath(rootDir, file) {
721
+ const normalized = file.replace(/\\/g, "/");
722
+ return resolvePath(rootDir, normalized);
723
+ }
724
+ function isProjectSourcePath(fileName, rootDir) {
725
+ const normalized = fileName.replace(/\\/g, "/");
726
+ const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
727
+ return normalized === root || normalized.startsWith(`${root}/`);
728
+ }
729
+ function isProjectSourceFile(sourceFile, rootDir) {
730
+ return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
103
731
  }
104
732
  function indexFile(sf, ctx) {
105
- for (const cls of sf.getClasses()) {
106
- const name = cls.getName();
733
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
734
+ const name = cls.name?.text;
107
735
  if (name && !ctx.classesByName.has(name)) {
108
- ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
736
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
109
737
  }
110
738
  }
111
- for (const statement of sf.getVariableStatements()) {
112
- for (const decl of statement.getDeclarations()) {
113
- const info = parseTokenVariable(decl, sf.getFilePath());
739
+ for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
740
+ for (const decl of statement.declarationList.declarations) {
741
+ const info = parseTokenVariable(decl, sf.fileName);
114
742
  if (info && !ctx.tokensByName.has(info.name)) {
115
743
  ctx.tokensByName.set(info.name, info);
116
744
  }
@@ -118,53 +746,105 @@ function indexFile(sf, ctx) {
118
746
  }
119
747
  }
120
748
  function parseTokenVariable(decl, file) {
121
- const init = decl.getInitializer();
122
- if (!init || !Node.isNewExpression(init))
749
+ const init = decl.initializer;
750
+ if (!init || !ts3.isNewExpression(init))
123
751
  return;
124
- if (init.getExpression().getText() !== "InjectionToken")
752
+ if (nodeText(init.expression) !== "InjectionToken")
125
753
  return;
126
- const [nameArg, optionsArg] = init.getArguments();
127
- const info = { name: decl.getName(), file };
128
- if (nameArg && Node.isStringLiteral(nameArg)) {
129
- info.stringName = nameArg.getLiteralText();
754
+ const [nameArg, optionsArg] = init.arguments ?? [];
755
+ const info = { name: variableName(decl), file, line: lineOf(decl) };
756
+ if (nameArg && ts3.isStringLiteral(nameArg)) {
757
+ info.stringName = nameArg.text;
130
758
  }
131
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
759
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
132
760
  const scope = stringLiteralProp(optionsArg, "scope");
133
- if (scope && SCOPES.includes(scope)) {
761
+ if (scope && isScope(scope)) {
134
762
  info.scope = scope;
135
763
  }
764
+ const providedIn = stringLiteralProp(optionsArg, "providedIn");
765
+ if (providedIn === "root") {
766
+ info.providedIn = "root";
767
+ }
768
+ const factory = getProp(optionsArg, "factory");
769
+ if (factory) {
770
+ info.hasFactory = true;
771
+ }
136
772
  }
137
773
  return info;
138
774
  }
139
775
  function parseModule(candidate, nameByNode, ctx) {
140
776
  const { options, className, file, line } = candidate;
141
777
  const name = nameByNode.get(candidate.node) ?? className;
142
- const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
778
+ const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
779
+ const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
143
780
  const imports = arrayProp(options, "imports").map((el) => {
144
- const decl = Node.isIdentifier(el) ? resolveDeclaration(el)[0] : undefined;
781
+ const unwrapped = unwrapForwardRef(el);
782
+ const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
145
783
  if (decl) {
146
784
  const known = nameByNode.get(decl);
147
785
  if (known)
148
786
  return known;
149
- if (Node.isClassDeclaration(decl)) {
787
+ if (ts3.isClassDeclaration(decl)) {
150
788
  const dec = findDecorator(decl, "Module");
151
789
  const decOptions = dec && decoratorObjectArg(dec);
152
790
  const decName = decOptions && stringLiteralProp(decOptions, "name");
153
- return decName ?? decl.getName() ?? el.getText();
791
+ return decName ?? decl.name?.text ?? nodeText(el);
154
792
  }
155
- if (Node.isVariableDeclaration(decl))
156
- return decl.getName();
793
+ if (ts3.isVariableDeclaration(decl))
794
+ return variableName(decl);
157
795
  }
158
- return el.getText();
796
+ return nodeText(el);
159
797
  }).filter((v, i, arr) => arr.indexOf(v) === i);
160
798
  const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
161
799
  const exportsSet = new Set(exports);
162
800
  const providers = [];
163
- for (const el of arrayProp(options, "providers")) {
801
+ for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
802
+ const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
803
+ if (parsedProviders) {
804
+ providers.push(...parsedProviders);
805
+ continue;
806
+ }
807
+ if (ts3.isCallExpression(el)) {
808
+ const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
809
+ warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
810
+ continue;
811
+ }
164
812
  const provider = parseProvider(el, exportsSet, ctx);
165
813
  if (provider)
166
814
  providers.push(provider);
167
815
  }
816
+ for (const el of arrayProp(options, "jobs")) {
817
+ if (!ts3.isIdentifier(el))
818
+ continue;
819
+ const decl = resolveDeclaration(el, ctx)[0];
820
+ if (!decl || !ts3.isClassDeclaration(decl))
821
+ continue;
822
+ const className2 = decl.name?.text ?? el.text;
823
+ const registeredProvider = providers.find((provider) => provider.token === className2);
824
+ if (registeredProvider)
825
+ continue;
826
+ const deps = classDeps(decl, ctx);
827
+ const injectable = parseInjectableOptions(decl, ctx);
828
+ const scope = injectable?.scope === "application" ? "application" : "job";
829
+ providers.push({
830
+ token: className2,
831
+ tokenKind: "class",
832
+ kind: "class",
833
+ useClass: className2,
834
+ scope,
835
+ deps: deps.deps,
836
+ optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
837
+ selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
838
+ skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
839
+ hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
840
+ functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
841
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
842
+ exported: exportsSet.has(className2),
843
+ file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
844
+ line: lineOf(decl),
845
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
846
+ });
847
+ }
168
848
  const controllers = [];
169
849
  for (const el of arrayProp(options, "controllers")) {
170
850
  const controller = parseController(el, ctx);
@@ -174,39 +854,59 @@ function parseModule(candidate, nameByNode, ctx) {
174
854
  const handlerClasses = [];
175
855
  const seenHandlers = new Set;
176
856
  const collectHandler = (expr) => {
177
- if (!Node.isIdentifier(expr))
857
+ if (!ts3.isIdentifier(expr))
178
858
  return;
179
- const decl = resolveDeclaration(expr)[0];
180
- if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
181
- seenHandlers.add(decl.getName() ?? "");
859
+ const decl = resolveDeclaration(expr, ctx)[0];
860
+ if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
861
+ seenHandlers.add(decl.name?.text ?? "");
182
862
  handlerClasses.push(decl);
183
863
  }
184
864
  };
185
865
  for (const el of arrayProp(options, "providers")) {
186
- if (Node.isIdentifier(el))
866
+ if (ts3.isIdentifier(el))
187
867
  collectHandler(el);
188
- if (Node.isObjectLiteralExpression(el)) {
868
+ if (ts3.isObjectLiteralExpression(el)) {
189
869
  const useClass = getProp(el, "useClass");
190
870
  if (useClass)
191
871
  collectHandler(useClass);
192
872
  }
193
873
  }
194
874
  arrayProp(options, "commands").forEach(collectHandler);
875
+ arrayProp(options, "jobs").forEach(collectHandler);
195
876
  arrayProp(options, "queries").forEach(collectHandler);
196
877
  const commands = [];
878
+ const jobs = [];
197
879
  const queries = [];
198
880
  for (const cls of handlerClasses) {
199
881
  const commandDec = findDecorator(cls, "Command");
200
882
  if (commandDec) {
201
883
  const meta = decoratorObjectArg(commandDec);
202
884
  if (meta) {
885
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
203
886
  commands.push({
204
- className: cls.getName() ?? "<anonymous>",
205
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
887
+ className: cls.name?.text ?? "<anonymous>",
888
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
206
889
  permission: stringLiteralProp(meta, "permission"),
207
890
  transaction: commandModeProp(meta, "transaction") ?? "none",
208
891
  audit: stringLiteralProp(meta, "audit"),
209
- idempotency: commandModeProp(meta, "idempotency") ?? "none"
892
+ idempotency: commandModeProp(meta, "idempotency") ?? "none",
893
+ ...booleanProp(meta, "standalone") ? { standalone: true } : {},
894
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
895
+ });
896
+ }
897
+ }
898
+ const jobDec = findDecorator(cls, "Job");
899
+ if (jobDec) {
900
+ const meta = decoratorObjectArg(jobDec);
901
+ if (meta) {
902
+ const injectable = parseInjectableOptions(cls, ctx);
903
+ const provider = providers.find((candidate2) => candidate2.token === (cls.name?.text ?? "<anonymous>"));
904
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${cls.name?.text ?? "<anonymous>"}`);
905
+ jobs.push({
906
+ className: cls.name?.text ?? "<anonymous>",
907
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
908
+ scope: provider?.scope ?? (injectable?.scope === "application" ? "application" : "job"),
909
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
210
910
  });
211
911
  }
212
912
  }
@@ -215,8 +915,8 @@ function parseModule(candidate, nameByNode, ctx) {
215
915
  const meta = decoratorObjectArg(queryDec);
216
916
  if (meta) {
217
917
  queries.push({
218
- className: cls.getName() ?? "<anonymous>",
219
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
918
+ className: cls.name?.text ?? "<anonymous>",
919
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
220
920
  });
221
921
  }
222
922
  }
@@ -231,7 +931,9 @@ function parseModule(candidate, nameByNode, ctx) {
231
931
  providers,
232
932
  controllers,
233
933
  commands,
934
+ jobs,
234
935
  queries,
936
+ ...aspects.length > 0 ? { aspects } : {},
235
937
  exports
236
938
  };
237
939
  }
@@ -240,13 +942,15 @@ function commandModeProp(object, name) {
240
942
  return value === "required" || value === "none" ? value : undefined;
241
943
  }
242
944
  function parseProvider(el, exportsSet, ctx) {
243
- const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
244
- const line = el.getStartLineNumber();
245
- if (Node.isIdentifier(el)) {
246
- const decl = resolveDeclaration(el)[0];
247
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
248
- const className = cls?.getName() ?? el.getText();
249
- const { deps, missing } = cls ? classDeps(cls, ctx) : { deps: [], missing: false };
945
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
946
+ const line = lineOf(el);
947
+ const unwrappedEl = unwrapForwardRef(el);
948
+ if (ts3.isIdentifier(unwrappedEl)) {
949
+ const decl = resolveDeclaration(unwrappedEl, ctx)[0];
950
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
951
+ const className = cls?.name?.text ?? unwrappedEl.text;
952
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
953
+ const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
250
954
  if (missing) {
251
955
  warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
252
956
  }
@@ -257,13 +961,20 @@ function parseProvider(el, exportsSet, ctx) {
257
961
  useClass: className,
258
962
  scope: resolveScope({ cls, tokenName: className }, ctx),
259
963
  deps,
964
+ optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
965
+ selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
966
+ skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
967
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
968
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
969
+ providedIn: injectable?.providedIn,
970
+ hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
260
971
  exported: exportsSet.has(className),
261
972
  file,
262
973
  line,
263
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
974
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
264
975
  };
265
976
  }
266
- if (!Node.isObjectLiteralExpression(el))
977
+ if (!ts3.isObjectLiteralExpression(el))
267
978
  return;
268
979
  const provideExpr = getProp(el, "provide");
269
980
  if (!provideExpr)
@@ -271,22 +982,43 @@ function parseProvider(el, exportsSet, ctx) {
271
982
  const { name: token, kind: tokenKind } = tokenNameOf(provideExpr, ctx);
272
983
  const explicitScope = parseScopeProp(el);
273
984
  const explicitDeps = arrayProp(el, "deps").map((d) => tokenNameOf(d, ctx).name);
985
+ const multi = booleanProp(el, "multi");
274
986
  const useClassExpr = getProp(el, "useClass");
275
987
  const useValueExpr = getProp(el, "useValue");
276
988
  const useFactoryExpr = getProp(el, "useFactory");
277
989
  const useExistingExpr = getProp(el, "useExisting");
278
990
  if (useClassExpr) {
279
- const decl = Node.isIdentifier(useClassExpr) ? resolveDeclaration(useClassExpr)[0] : undefined;
280
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
281
- const useClass = cls?.getName() ?? useClassExpr.getText();
991
+ const unwrappedClass = unwrapForwardRef(useClassExpr);
992
+ const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
993
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
994
+ const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
282
995
  let deps = explicitDeps;
283
- if (deps.length === 0 && cls) {
996
+ let optionalDeps = [];
997
+ let selfDeps = [];
998
+ let skipSelfDeps = [];
999
+ let hostDeps = [];
1000
+ let functionalInjects = [];
1001
+ if (cls) {
284
1002
  const result = classDeps(cls, ctx);
285
- deps = result.deps;
1003
+ if (deps.length === 0) {
1004
+ deps = result.deps;
1005
+ optionalDeps = result.optionalDeps;
1006
+ selfDeps = result.selfDeps;
1007
+ skipSelfDeps = result.skipSelfDeps;
1008
+ hostDeps = result.hostDeps;
1009
+ } else {
1010
+ optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
1011
+ selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
1012
+ skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
1013
+ hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
1014
+ }
1015
+ functionalInjects = result.functionalInjects;
286
1016
  if (result.missing) {
287
1017
  warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
288
1018
  }
289
1019
  }
1020
+ const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
1021
+ validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
290
1022
  return {
291
1023
  token,
292
1024
  tokenKind,
@@ -294,31 +1026,42 @@ function parseProvider(el, exportsSet, ctx) {
294
1026
  useClass,
295
1027
  scope: resolveScope({ explicit: explicitScope, cls, tokenName: token }, ctx),
296
1028
  deps,
1029
+ optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
1030
+ selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
1031
+ skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1032
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1033
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
1034
+ multi: multi ?? undefined,
1035
+ providedIn: injectable?.providedIn,
1036
+ hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
297
1037
  exported: exportsSet.has(token),
298
1038
  file,
299
1039
  line,
300
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1040
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
301
1041
  };
302
1042
  }
303
1043
  if (useValueExpr) {
1044
+ validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
304
1045
  return {
305
1046
  token,
306
1047
  tokenKind,
307
1048
  kind: "value",
308
- useValueExpr: useValueExpr.getText(),
1049
+ useValueExpr: nodeText(useValueExpr),
309
1050
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
310
1051
  deps: [],
1052
+ multi: multi ?? undefined,
311
1053
  exported: exportsSet.has(token),
312
1054
  file,
313
1055
  line,
314
- importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
1056
+ importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
315
1057
  };
316
1058
  }
317
1059
  if (useFactoryExpr) {
318
- const factoryName = Node.isIdentifier(useFactoryExpr) ? (() => {
319
- const decl = resolveDeclaration(useFactoryExpr)[0];
320
- return decl && (Node.isFunctionDeclaration(decl) || Node.isVariableDeclaration(decl)) ? decl.getName() ?? useFactoryExpr.getText() : useFactoryExpr.getText();
321
- })() : useFactoryExpr.getText();
1060
+ const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
1061
+ const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
1062
+ return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
1063
+ })() : nodeText(useFactoryExpr);
1064
+ validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
322
1065
  return {
323
1066
  token,
324
1067
  tokenKind,
@@ -326,14 +1069,16 @@ function parseProvider(el, exportsSet, ctx) {
326
1069
  useFactoryName: factoryName,
327
1070
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
328
1071
  deps: explicitDeps,
1072
+ multi: multi ?? undefined,
329
1073
  exported: exportsSet.has(token),
330
1074
  file,
331
1075
  line,
332
- importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
1076
+ importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
333
1077
  };
334
1078
  }
335
1079
  if (useExistingExpr) {
336
1080
  const target = tokenNameOf(useExistingExpr, ctx).name;
1081
+ validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
337
1082
  return {
338
1083
  token,
339
1084
  tokenKind,
@@ -341,6 +1086,7 @@ function parseProvider(el, exportsSet, ctx) {
341
1086
  useExisting: target,
342
1087
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
343
1088
  deps: [target],
1089
+ multi: multi ?? undefined,
344
1090
  exported: exportsSet.has(token),
345
1091
  file,
346
1092
  line
@@ -348,99 +1094,654 @@ function parseProvider(el, exportsSet, ctx) {
348
1094
  }
349
1095
  return;
350
1096
  }
351
- function parseController(el, ctx) {
352
- if (!Node.isIdentifier(el))
1097
+ function expandProviderExpressions(expressions, ctx, seen = new Set) {
1098
+ const result = [];
1099
+ for (const expression of expressions) {
1100
+ if (ts3.isSpreadElement(expression)) {
1101
+ result.push(...expandProviderExpressions([expression.expression], ctx, seen));
1102
+ continue;
1103
+ }
1104
+ if (ts3.isIdentifier(expression)) {
1105
+ const declaration = resolveDeclaration(expression, ctx)[0];
1106
+ if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
1107
+ const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
1108
+ if (seen.has(key))
1109
+ continue;
1110
+ const initializer = declaration.initializer;
1111
+ if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
1112
+ const nested = initializer.arguments[0];
1113
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1114
+ seen.add(key);
1115
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1116
+ seen.delete(key);
1117
+ continue;
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
1123
+ const nested = expression.arguments[0];
1124
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1125
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1126
+ continue;
1127
+ }
1128
+ }
1129
+ result.push(expression);
1130
+ }
1131
+ return result;
1132
+ }
1133
+ function isProviderHelper(expression, name) {
1134
+ return nodeText(expression.expression).split(".").pop() === name;
1135
+ }
1136
+ function parseFunctionalProvider(expression, exportsSet, ctx) {
1137
+ if (!ts3.isCallExpression(expression))
1138
+ return;
1139
+ const helper = nodeText(expression.expression).split(".").pop();
1140
+ const args = expression.arguments;
1141
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1142
+ const line = lineOf(expression);
1143
+ if (helper === "provideToken") {
1144
+ const tokenExpr = args[0];
1145
+ const valueExpr = args[1];
1146
+ if (!tokenExpr || !valueExpr)
1147
+ return [];
1148
+ const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
1149
+ validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
1150
+ return [{
1151
+ token,
1152
+ tokenKind,
1153
+ kind: "value",
1154
+ useValueExpr: nodeText(valueExpr),
1155
+ scope: resolveScope({ tokenName: token }, ctx),
1156
+ deps: [],
1157
+ exported: exportsSet.has(token),
1158
+ file,
1159
+ line,
1160
+ importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
1161
+ }];
1162
+ }
1163
+ if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
1164
+ const initializer = args[0];
1165
+ if (!initializer)
1166
+ return [];
1167
+ const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
1168
+ return [{
1169
+ token,
1170
+ tokenKind: "injection-token",
1171
+ kind: "value",
1172
+ useValueExpr: nodeText(initializer),
1173
+ scope: "application",
1174
+ deps: [],
1175
+ multi: true,
1176
+ exported: false,
1177
+ file,
1178
+ line,
1179
+ importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
1180
+ }];
1181
+ }
1182
+ if (helper === "provideRouter") {
1183
+ const providers = [];
1184
+ const routes = args[0];
1185
+ if (routes) {
1186
+ providers.push({
1187
+ token: "ROUTE_CONFIG",
1188
+ tokenKind: "injection-token",
1189
+ kind: "value",
1190
+ useValueExpr: nodeText(routes),
1191
+ scope: "application",
1192
+ deps: [],
1193
+ exported: false,
1194
+ file,
1195
+ line,
1196
+ importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
1197
+ });
1198
+ }
1199
+ for (const feature of args.slice(1)) {
1200
+ if (!ts3.isCallExpression(feature))
1201
+ continue;
1202
+ const featureName = nodeText(feature.expression).split(".").pop();
1203
+ if (featureName === "withRouterConfig" && feature.arguments[0]) {
1204
+ providers.push({
1205
+ token: "ROUTER_CONFIGURATION",
1206
+ tokenKind: "injection-token",
1207
+ kind: "value",
1208
+ useValueExpr: nodeText(feature.arguments[0]),
1209
+ scope: "application",
1210
+ deps: [],
1211
+ exported: false,
1212
+ file,
1213
+ line
1214
+ });
1215
+ } else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
1216
+ const strategy = feature.arguments[0];
1217
+ const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
1218
+ providers.push({
1219
+ token: "TITLE_STRATEGY",
1220
+ tokenKind: "injection-token",
1221
+ kind: isClass ? "class" : "value",
1222
+ ...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
1223
+ scope: "application",
1224
+ deps: [],
1225
+ exported: false,
1226
+ file,
1227
+ line,
1228
+ importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
1229
+ });
1230
+ }
1231
+ }
1232
+ return providers;
1233
+ }
1234
+ if (helper === "provideHttpClient") {
1235
+ const providers = [{
1236
+ token: "HttpClient",
1237
+ tokenKind: "class",
1238
+ kind: "class",
1239
+ useClass: "HttpClient",
1240
+ scope: "application",
1241
+ deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1242
+ optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1243
+ exported: false,
1244
+ file,
1245
+ line,
1246
+ importModule: "@supacloud/app"
1247
+ }];
1248
+ for (const feature of args) {
1249
+ if (!ts3.isCallExpression(feature))
1250
+ continue;
1251
+ const featureName = nodeText(feature.expression).split(".").pop();
1252
+ if (featureName === "withInterceptors") {
1253
+ for (const interceptorArg of feature.arguments) {
1254
+ const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
1255
+ for (const value of values) {
1256
+ providers.push({
1257
+ token: "HTTP_INTERCEPTORS",
1258
+ tokenKind: "injection-token",
1259
+ kind: "value",
1260
+ useValueExpr: nodeText(value),
1261
+ scope: "application",
1262
+ deps: [],
1263
+ multi: true,
1264
+ exported: false,
1265
+ file,
1266
+ line,
1267
+ importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
1268
+ });
1269
+ }
1270
+ }
1271
+ } else if (featureName === "withFetch" && feature.arguments.length > 0) {
1272
+ warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
1273
+ }
1274
+ }
1275
+ return providers;
1276
+ }
1277
+ return;
1278
+ }
1279
+ function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
1280
+ const expected = providerTokenValueType(provideExpr, ctx);
1281
+ const actual = providerImplementationType(implementationExpr, kind, ctx);
1282
+ if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
353
1283
  return;
354
- const decl = resolveDeclaration(el)[0];
355
- if (!decl || !Node.isClassDeclaration(decl))
1284
+ if (ctx.checker.isTypeAssignableTo(actual, expected))
1285
+ return;
1286
+ const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
1287
+ ctx.diagnostics.push({
1288
+ severity: "error",
1289
+ code: "provider-type-mismatch",
1290
+ message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
1291
+ file,
1292
+ line,
1293
+ errorCode: "SC2010",
1294
+ docsUrl: "https://supacloud.dev/errors/SC2010"
1295
+ });
1296
+ }
1297
+ function providerTokenValueType(expr, ctx) {
1298
+ const type = ctx.checker.getTypeAtLocation(expr);
1299
+ const typeArguments = typeArgumentsOf(type, ctx);
1300
+ if (typeArguments.length > 0)
1301
+ return typeArguments[0];
1302
+ if (ts3.isIdentifier(expr)) {
1303
+ const declaration = resolveDeclaration(expr, ctx)[0];
1304
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1305
+ return declaredClassType(declaration, ctx);
1306
+ }
1307
+ }
1308
+ return;
1309
+ }
1310
+ function providerImplementationType(expr, kind, ctx) {
1311
+ if (kind === "class" || kind === "existing") {
1312
+ if (ts3.isIdentifier(expr)) {
1313
+ const declaration = resolveDeclaration(expr, ctx)[0];
1314
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1315
+ return declaredClassType(declaration, ctx);
1316
+ }
1317
+ }
1318
+ const type = ctx.checker.getTypeAtLocation(expr);
1319
+ const typeArguments = typeArgumentsOf(type, ctx);
1320
+ return typeArguments.length > 0 ? typeArguments[0] : undefined;
1321
+ }
1322
+ if (kind === "factory") {
1323
+ const type = ctx.checker.getTypeAtLocation(expr);
1324
+ const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
1325
+ return signature?.getReturnType();
1326
+ }
1327
+ return ctx.checker.getTypeAtLocation(expr);
1328
+ }
1329
+ function declaredClassType(declaration, ctx) {
1330
+ const name = declaration.name;
1331
+ if (!name)
1332
+ return;
1333
+ const symbol = ctx.checker.getSymbolAtLocation(name);
1334
+ return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
1335
+ }
1336
+ function typeArgumentsOf(type, ctx) {
1337
+ return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
1338
+ }
1339
+ function isTypeReference(type) {
1340
+ return "target" in type;
1341
+ }
1342
+ function isUnknownOrAny(type) {
1343
+ return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
1344
+ }
1345
+ function parseController(input, ctx) {
1346
+ let decl;
1347
+ if (ts3.isClassDeclaration(input)) {
1348
+ decl = input;
1349
+ } else {
1350
+ const unwrapped = unwrapForwardRef(input);
1351
+ const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
1352
+ if (resolved && ts3.isClassDeclaration(resolved)) {
1353
+ decl = resolved;
1354
+ }
1355
+ }
1356
+ if (!decl)
356
1357
  return;
357
1358
  const controllerDec = findDecorator(decl, "Controller");
358
1359
  if (!controllerDec)
359
1360
  return;
360
- const pathArg = controllerDec.getArguments()[0];
361
- const path = pathArg && Node.isStringLiteral(pathArg) ? pathArg.getLiteralText() : "/";
362
- const { deps, missing } = classDeps(decl, ctx);
363
- const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1361
+ let path = "/";
1362
+ let standalone;
1363
+ const pathArg = decoratorArguments(controllerDec)[0];
1364
+ if (pathArg) {
1365
+ if (ts3.isStringLiteral(pathArg)) {
1366
+ path = pathArg.text;
1367
+ } else if (ts3.isObjectLiteralExpression(pathArg)) {
1368
+ const p = stringLiteralProp(pathArg, "path");
1369
+ if (p)
1370
+ path = p;
1371
+ standalone = booleanProp(pathArg, "standalone");
1372
+ }
1373
+ }
1374
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
1375
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
364
1376
  if (missing) {
365
- warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
1377
+ warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
366
1378
  }
367
1379
  const injectable = parseInjectableOptions(decl, ctx);
368
1380
  const routes = [];
369
1381
  const schemaImports = {};
370
- for (const method of decl.getMethods()) {
371
- for (const dec of method.getDecorators()) {
372
- const name = decoratorName(dec);
1382
+ const classGuards = [];
1383
+ for (const dec of decoratorsOf(decl)) {
1384
+ if (decoratorName2(dec) === "UseGuards") {
1385
+ for (const gArg of decoratorArguments(dec)) {
1386
+ classGuards.push(tokenText(gArg, ctx));
1387
+ }
1388
+ }
1389
+ }
1390
+ for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
1391
+ for (const dec of decoratorsOf(method)) {
1392
+ const name = decoratorName2(dec);
373
1393
  const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
374
1394
  if (!httpMethod)
375
1395
  continue;
376
- const args = dec.getArguments();
1396
+ const args = decoratorArguments(dec);
377
1397
  const pathArg2 = args[0];
1398
+ const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
378
1399
  const route = {
379
1400
  method: httpMethod,
380
- path: pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/",
381
- handler: method.getName()
1401
+ path: routePath,
1402
+ handler: propertyName(method.name)
382
1403
  };
1404
+ const pathParams = [];
1405
+ const paramRegex = /:([a-zA-Z0-9_]+)/g;
1406
+ let match;
1407
+ while ((match = paramRegex.exec(routePath)) !== null) {
1408
+ pathParams.push(match[1]);
1409
+ }
1410
+ if (pathParams.length > 0)
1411
+ route.pathParams = pathParams;
1412
+ const paramBindings = [];
1413
+ const queryBindings = [];
1414
+ const paramTransforms = {};
1415
+ const paramDefaults = {};
1416
+ const queryTransforms = {};
1417
+ const queryDefaults = {};
1418
+ let hasBodyBinding = false;
1419
+ const handlerParams = [];
1420
+ for (const p of method.parameters) {
1421
+ const pName = parameterName(p);
1422
+ let hasBindingDecorator = false;
1423
+ let paramNode;
1424
+ for (const pDec of decoratorsOf(p)) {
1425
+ const dName = decoratorName2(pDec);
1426
+ const dArgs = decoratorArguments(pDec);
1427
+ if (dName === "Param") {
1428
+ hasBindingDecorator = true;
1429
+ const parsed = parseBindingOptions(dArgs, pName);
1430
+ paramBindings.push(parsed.name);
1431
+ if (parsed.transform)
1432
+ paramTransforms[parsed.name] = parsed.transform;
1433
+ if (parsed.default !== undefined)
1434
+ paramDefaults[parsed.name] = parsed.default;
1435
+ paramNode = {
1436
+ name: pName,
1437
+ kind: "param",
1438
+ bindingName: parsed.name,
1439
+ transform: parsed.transform,
1440
+ default: parsed.default
1441
+ };
1442
+ } else if (dName === "Query") {
1443
+ hasBindingDecorator = true;
1444
+ const parsed = parseBindingOptions(dArgs, pName);
1445
+ queryBindings.push(parsed.name);
1446
+ if (parsed.transform)
1447
+ queryTransforms[parsed.name] = parsed.transform;
1448
+ if (parsed.default !== undefined)
1449
+ queryDefaults[parsed.name] = parsed.default;
1450
+ paramNode = {
1451
+ name: pName,
1452
+ kind: "query",
1453
+ bindingName: parsed.name,
1454
+ transform: parsed.transform,
1455
+ default: parsed.default
1456
+ };
1457
+ } else if (dName === "Body") {
1458
+ hasBindingDecorator = true;
1459
+ hasBodyBinding = true;
1460
+ paramNode = { name: pName, kind: "body" };
1461
+ } else if (dName === "Headers") {
1462
+ hasBindingDecorator = true;
1463
+ paramNode = { name: pName, kind: "headers" };
1464
+ }
1465
+ }
1466
+ if (!hasBindingDecorator && pathParams.includes(pName)) {
1467
+ paramBindings.push(pName);
1468
+ const typeText = p.type ? nodeText(p.type) : "";
1469
+ let inferredTransform;
1470
+ if (typeText === "number") {
1471
+ paramTransforms[pName] = "number";
1472
+ inferredTransform = "number";
1473
+ } else if (typeText === "boolean") {
1474
+ paramTransforms[pName] = "boolean";
1475
+ inferredTransform = "boolean";
1476
+ }
1477
+ paramNode = {
1478
+ name: pName,
1479
+ kind: "param",
1480
+ bindingName: pName,
1481
+ transform: inferredTransform
1482
+ };
1483
+ } else if (!hasBindingDecorator) {
1484
+ if (pName === "req" || pName === "ctx" || pName === "context") {
1485
+ paramNode = { name: pName, kind: "context" };
1486
+ } else {
1487
+ paramNode = { name: pName, kind: "unknown" };
1488
+ }
1489
+ }
1490
+ if (paramNode)
1491
+ handlerParams.push(paramNode);
1492
+ }
1493
+ if (paramBindings.length > 0)
1494
+ route.paramBindings = paramBindings;
1495
+ if (queryBindings.length > 0)
1496
+ route.queryBindings = queryBindings;
1497
+ if (Object.keys(paramTransforms).length > 0)
1498
+ route.paramTransforms = paramTransforms;
1499
+ if (Object.keys(paramDefaults).length > 0)
1500
+ route.paramDefaults = paramDefaults;
1501
+ if (Object.keys(queryTransforms).length > 0)
1502
+ route.queryTransforms = queryTransforms;
1503
+ if (Object.keys(queryDefaults).length > 0)
1504
+ route.queryDefaults = queryDefaults;
1505
+ if (hasBodyBinding)
1506
+ route.hasBodyBinding = true;
1507
+ if (handlerParams.length > 0)
1508
+ route.handlerParams = handlerParams;
1509
+ const routeGuards = [...classGuards];
1510
+ const routeCanDeactivate = [];
1511
+ for (const mDec of decoratorsOf(method)) {
1512
+ const dName = decoratorName2(mDec);
1513
+ const mArgs = decoratorArguments(mDec);
1514
+ if (dName === "UseGuards") {
1515
+ for (const gArg of mArgs) {
1516
+ routeGuards.push(tokenText(gArg, ctx));
1517
+ }
1518
+ } else if (dName === "CanDeactivate") {
1519
+ for (const gArg of mArgs) {
1520
+ routeCanDeactivate.push(tokenText(gArg, ctx));
1521
+ }
1522
+ } else if (dName === "Title") {
1523
+ const tArg = mArgs[0];
1524
+ if (tArg && ts3.isStringLiteral(tArg)) {
1525
+ route.title = tArg.text;
1526
+ }
1527
+ } else if (dName === "Data") {
1528
+ const dArg = mArgs[0];
1529
+ if (dArg && ts3.isObjectLiteralExpression(dArg)) {
1530
+ route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
1531
+ }
1532
+ } else if (dName === "Resolve") {
1533
+ const rArg = mArgs[0];
1534
+ if (rArg && ts3.isObjectLiteralExpression(rArg)) {
1535
+ const resolvers = route.resolvers ?? {};
1536
+ for (const prop of rArg.properties) {
1537
+ if (ts3.isPropertyAssignment(prop)) {
1538
+ const rName = propertyName(prop.name);
1539
+ const init = prop.initializer;
1540
+ if (init)
1541
+ resolvers[rName] = tokenText(init, ctx);
1542
+ }
1543
+ }
1544
+ if (Object.keys(resolvers).length > 0) {
1545
+ route.resolvers = resolvers;
1546
+ }
1547
+ }
1548
+ }
1549
+ }
383
1550
  const optionsArg = args[1];
384
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
1551
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
385
1552
  for (const field of ["body", "params", "query", "response"]) {
386
1553
  const schemaExpr = getProp(optionsArg, field);
387
- if (schemaExpr && Node.isIdentifier(schemaExpr)) {
388
- route[field] = schemaExpr.getText();
1554
+ if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
1555
+ route[field] = nodeText(schemaExpr);
389
1556
  const importPath = importPathOf(schemaExpr, ctx);
390
1557
  if (importPath)
391
- schemaImports[schemaExpr.getText()] = importPath;
1558
+ schemaImports[schemaExpr.text] = importPath;
392
1559
  }
393
1560
  }
394
1561
  const commandExpr = getProp(optionsArg, "command");
395
- if (commandExpr && Node.isIdentifier(commandExpr)) {
396
- const commandDecl = resolveDeclaration(commandExpr)[0];
397
- route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
1562
+ if (commandExpr && ts3.isIdentifier(commandExpr)) {
1563
+ const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
1564
+ route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
1565
+ }
1566
+ const guardsExpr = getProp(optionsArg, "guards");
1567
+ if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
1568
+ for (const el of guardsExpr.elements) {
1569
+ routeGuards.push(tokenText(el, ctx));
1570
+ }
1571
+ }
1572
+ const canMatchExpr = getProp(optionsArg, "canMatch");
1573
+ if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
1574
+ const canMatchList = [];
1575
+ for (const el of canMatchExpr.elements) {
1576
+ canMatchList.push(tokenText(el, ctx));
1577
+ }
1578
+ if (canMatchList.length > 0) {
1579
+ route.canMatch = canMatchList;
1580
+ }
1581
+ }
1582
+ const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
1583
+ if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
1584
+ for (const el of canDeactivateExpr.elements) {
1585
+ routeCanDeactivate.push(tokenText(el, ctx));
1586
+ }
398
1587
  }
1588
+ const resolversExpr = getProp(optionsArg, "resolvers");
1589
+ if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
1590
+ const resolvers = {};
1591
+ for (const prop of resolversExpr.properties) {
1592
+ if (ts3.isPropertyAssignment(prop)) {
1593
+ const rName = propertyName(prop.name);
1594
+ const init = prop.initializer;
1595
+ if (init)
1596
+ resolvers[rName] = tokenText(init, ctx);
1597
+ }
1598
+ }
1599
+ if (Object.keys(resolvers).length > 0) {
1600
+ route.resolvers = resolvers;
1601
+ }
1602
+ }
1603
+ const redirectToExpr = getProp(optionsArg, "redirectTo");
1604
+ if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
1605
+ route.redirectTo = redirectToExpr.text;
1606
+ }
1607
+ const pathMatchExpr = getProp(optionsArg, "pathMatch");
1608
+ if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
1609
+ const val = pathMatchExpr.text;
1610
+ if (val === "full" || val === "prefix") {
1611
+ route.pathMatch = val;
1612
+ }
1613
+ }
1614
+ const titleExpr = getProp(optionsArg, "title");
1615
+ if (titleExpr && ts3.isStringLiteral(titleExpr)) {
1616
+ route.title = titleExpr.text;
1617
+ }
1618
+ const dataExpr = getProp(optionsArg, "data");
1619
+ if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
1620
+ route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
1621
+ }
1622
+ const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
1623
+ if (aspects.length > 0)
1624
+ route.aspects = aspects;
1625
+ }
1626
+ if (routeGuards.length > 0) {
1627
+ route.guards = routeGuards;
1628
+ }
1629
+ if (routeCanDeactivate.length > 0) {
1630
+ route.canDeactivate = routeCanDeactivate;
399
1631
  }
400
1632
  routes.push(route);
401
1633
  }
402
1634
  }
403
1635
  return {
404
- className: decl.getName() ?? "<anonymous>",
1636
+ className: decl.name?.text ?? "<anonymous>",
405
1637
  path,
406
1638
  scope: injectable?.scope ?? "request",
407
1639
  deps,
1640
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
1641
+ optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
1642
+ selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
1643
+ skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1644
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1645
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
1646
+ standalone: standalone || undefined,
408
1647
  routes,
409
1648
  file,
410
- importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
1649
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
411
1650
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
412
1651
  };
413
1652
  }
414
1653
  function classDeps(cls, ctx) {
415
1654
  const injectable = parseInjectableOptions(cls, ctx);
416
- if (injectable?.deps)
417
- return { deps: injectable.deps, missing: false };
418
- const ctor = cls.getConstructors()[0];
419
- if (!ctor || ctor.getParameters().length === 0)
420
- return { deps: [], missing: false };
421
- const injectParams = parseInjectParams(cls);
422
- const deps = [];
1655
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1656
+ const deps = injectable?.deps ? [...injectable.deps] : [];
1657
+ const optionalDeps = [];
1658
+ const selfDeps = [];
1659
+ const skipSelfDeps = [];
1660
+ const hostDeps = [];
1661
+ const functionalInjects = [];
423
1662
  let missing = false;
424
- ctor.getParameters().forEach((param, index) => {
425
- const injected = injectParams.get(index);
426
- if (injected) {
427
- deps.push(injected);
428
- return;
429
- }
430
- const byType = paramTypeTokenName(param, ctx);
431
- if (byType) {
432
- deps.push(byType);
433
- } else {
434
- missing = true;
1663
+ if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
1664
+ const injectParams = parseInjectParams(cls, ctx);
1665
+ const optionalIndices = parseOptionalParams(cls);
1666
+ const selfIndices = parseModifierParams(cls, "Self");
1667
+ const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
1668
+ const hostIndices = parseModifierParams(cls, "Host");
1669
+ ctor.parameters.forEach((param, index) => {
1670
+ const isOptional = optionalIndices.has(index);
1671
+ const injected = injectParams.get(index);
1672
+ const tokenName = injected ?? paramTypeTokenName(param, ctx);
1673
+ if (tokenName) {
1674
+ deps.push(tokenName);
1675
+ if (isOptional)
1676
+ optionalDeps.push(tokenName);
1677
+ if (selfIndices.has(index))
1678
+ selfDeps.push(tokenName);
1679
+ if (skipSelfIndices.has(index))
1680
+ skipSelfDeps.push(tokenName);
1681
+ if (hostIndices.has(index))
1682
+ hostDeps.push(tokenName);
1683
+ } else {
1684
+ if (!isOptional)
1685
+ missing = true;
1686
+ }
1687
+ });
1688
+ }
1689
+ for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
1690
+ const init = prop.initializer;
1691
+ if (init && ts3.isCallExpression(init)) {
1692
+ const callName = nodeText(init.expression).split(".").pop();
1693
+ if (callName === "inject") {
1694
+ const [tokenArg, optionsArg] = init.arguments;
1695
+ if (tokenArg) {
1696
+ const tokenName = tokenText(tokenArg, ctx);
1697
+ const unwrappedToken = unwrapForwardRef(tokenArg);
1698
+ const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
1699
+ if (!known) {
1700
+ missing = true;
1701
+ continue;
1702
+ }
1703
+ if (!deps.includes(tokenName))
1704
+ deps.push(tokenName);
1705
+ const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
1706
+ optional: booleanProp(optionsArg, "optional") ?? false,
1707
+ self: booleanProp(optionsArg, "self") ?? false,
1708
+ skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
1709
+ host: booleanProp(optionsArg, "host") ?? false
1710
+ } : { optional: false, self: false, skipSelf: false, host: false };
1711
+ if (options.optional && !optionalDeps.includes(tokenName))
1712
+ optionalDeps.push(tokenName);
1713
+ if (options.self && !selfDeps.includes(tokenName))
1714
+ selfDeps.push(tokenName);
1715
+ if (options.skipSelf && !skipSelfDeps.includes(tokenName))
1716
+ skipSelfDeps.push(tokenName);
1717
+ if (options.host && !hostDeps.includes(tokenName))
1718
+ hostDeps.push(tokenName);
1719
+ if (!functionalInjects.some((entry) => entry.token === tokenName)) {
1720
+ functionalInjects.push({
1721
+ token: tokenName,
1722
+ expression: nodeText(unwrappedToken),
1723
+ importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
1724
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1725
+ return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
1726
+ })() : undefined,
1727
+ importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
1728
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1729
+ return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
1730
+ })() : undefined,
1731
+ ...options
1732
+ });
1733
+ }
1734
+ }
1735
+ }
435
1736
  }
436
- });
437
- return { deps, missing };
1737
+ }
1738
+ return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
438
1739
  }
439
1740
  function paramTypeTokenName(param, ctx) {
440
- const typeNode = param.getTypeNode();
1741
+ const typeNode = param.type;
441
1742
  if (!typeNode)
442
1743
  return;
443
- const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
1744
+ const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
444
1745
  if (ctx.classesByName.has(text))
445
1746
  return text;
446
1747
  if (ctx.tokensByName.has(text))
@@ -455,37 +1756,85 @@ function parseInjectableOptions(cls, ctx) {
455
1756
  if (!obj)
456
1757
  return {};
457
1758
  const scope = stringLiteralProp(obj, "scope");
1759
+ const providedIn = stringLiteralProp(obj, "providedIn");
458
1760
  const depsExpr = getProp(obj, "deps");
459
1761
  return {
460
- scope: scope && SCOPES.includes(scope) ? scope : undefined,
461
- deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
1762
+ scope: scope && isScope(scope) ? scope : undefined,
1763
+ providedIn: providedIn === "root" ? "root" : undefined,
1764
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
462
1765
  };
463
1766
  }
464
- function parseInjectParams(cls) {
1767
+ function parseInjectParams(cls, ctx) {
465
1768
  const result = new Map;
466
- const ctor = cls.getConstructors()[0];
1769
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
467
1770
  if (!ctor)
468
1771
  return result;
469
- ctor.getParameters().forEach((param, index) => {
470
- for (const dec of param.getDecorators()) {
471
- if (decoratorName(dec) !== "Inject")
1772
+ ctor.parameters.forEach((param, index) => {
1773
+ for (const dec of decoratorsOf(param)) {
1774
+ if (decoratorName2(dec) !== "Inject")
472
1775
  continue;
473
- const arg = dec.getArguments()[0];
1776
+ const arg = decoratorArguments(dec)[0];
474
1777
  if (arg)
475
- result.set(index, tokenText(arg));
1778
+ result.set(index, tokenText(arg, ctx));
1779
+ }
1780
+ });
1781
+ return result;
1782
+ }
1783
+ function parseOptionalParams(cls) {
1784
+ const result = new Set;
1785
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1786
+ if (!ctor)
1787
+ return result;
1788
+ ctor.parameters.forEach((param, index) => {
1789
+ for (const dec of decoratorsOf(param)) {
1790
+ if (decoratorName2(dec) === "Optional")
1791
+ result.add(index);
1792
+ }
1793
+ if (param.questionToken)
1794
+ result.add(index);
1795
+ });
1796
+ return result;
1797
+ }
1798
+ function parseModifierParams(cls, modifierName) {
1799
+ const result = new Set;
1800
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1801
+ if (!ctor)
1802
+ return result;
1803
+ ctor.parameters.forEach((param, index) => {
1804
+ for (const dec of decoratorsOf(param)) {
1805
+ if (decoratorName2(dec) === modifierName)
1806
+ result.add(index);
476
1807
  }
477
1808
  });
478
1809
  return result;
479
1810
  }
480
- function tokenText(expr) {
481
- if (Node.isIdentifier(expr)) {
482
- const decl = resolveDeclaration(expr)[0];
483
- if (decl && Node.isClassDeclaration(decl))
484
- return decl.getName() ?? expr.getText();
485
- if (decl && Node.isVariableDeclaration(decl))
486
- return decl.getName();
1811
+ function unwrapForwardRef(expr) {
1812
+ if (ts3.isCallExpression(expr)) {
1813
+ const exprText = nodeText(expr.expression);
1814
+ if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
1815
+ const arg = expr.arguments[0];
1816
+ if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
1817
+ const body = arg.body;
1818
+ if (body && ts3.isExpression(body)) {
1819
+ return unwrapForwardRef(body);
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+ return expr;
1825
+ }
1826
+ function tokenText(expr, ctx) {
1827
+ const unwrapped = unwrapForwardRef(expr);
1828
+ if (ts3.isStringLiteral(unwrapped))
1829
+ return unwrapped.text;
1830
+ if (ts3.isIdentifier(unwrapped)) {
1831
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1832
+ if (decl && ts3.isClassDeclaration(decl))
1833
+ return decl.name?.text ?? unwrapped.text;
1834
+ if (decl && ts3.isVariableDeclaration(decl))
1835
+ return variableName(decl);
487
1836
  }
488
- return expr.getText();
1837
+ return nodeText(unwrapped);
489
1838
  }
490
1839
  function resolveScope(input, ctx) {
491
1840
  if (input.explicit)
@@ -501,88 +1850,252 @@ function resolveScope(input, ctx) {
501
1850
  return "application";
502
1851
  }
503
1852
  function tokenNameOf(expr, ctx) {
504
- if (Node.isIdentifier(expr)) {
505
- const decl = resolveDeclaration(expr)[0];
506
- if (decl && Node.isClassDeclaration(decl)) {
507
- return { name: decl.getName() ?? expr.getText(), kind: "class" };
1853
+ const unwrapped = unwrapForwardRef(expr);
1854
+ if (ts3.isIdentifier(unwrapped)) {
1855
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1856
+ if (decl && ts3.isClassDeclaration(decl)) {
1857
+ return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
508
1858
  }
509
- if (decl && Node.isVariableDeclaration(decl)) {
510
- const name = decl.getName();
1859
+ if (decl && ts3.isVariableDeclaration(decl)) {
1860
+ const name = variableName(decl);
511
1861
  return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
512
1862
  }
513
- if (ctx.tokensByName.has(expr.getText())) {
514
- return { name: expr.getText(), kind: "injection-token" };
1863
+ if (ctx.tokensByName.has(unwrapped.text)) {
1864
+ return { name: unwrapped.text, kind: "injection-token" };
515
1865
  }
516
1866
  }
517
- return { name: expr.getText(), kind: "class" };
1867
+ return { name: nodeText(expr), kind: "class" };
518
1868
  }
519
- function resolveDeclaration(id) {
520
- let symbol = id.getSymbol();
1869
+ function resolveDeclaration(id, ctx) {
1870
+ let symbol = ctx.checker.getSymbolAtLocation(id);
521
1871
  if (!symbol)
522
1872
  return [];
523
- let declarations = symbol.getDeclarations();
1873
+ let declarations = symbol.declarations ?? [];
524
1874
  for (let guard = 0;guard < 4; guard += 1) {
525
- const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
1875
+ const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
526
1876
  if (!isAlias)
527
1877
  break;
528
- const aliased = symbol.getAliasedSymbol();
529
- if (!aliased)
1878
+ if (!(symbol.flags & ts3.SymbolFlags.Alias))
530
1879
  break;
1880
+ const aliased = ctx.checker.getAliasedSymbol(symbol);
531
1881
  symbol = aliased;
532
- declarations = aliased.getDeclarations();
1882
+ declarations = aliased.declarations ?? [];
533
1883
  }
534
1884
  return declarations;
535
1885
  }
536
1886
  function importPathOf(id, ctx) {
537
- const symbol = id.getSymbol();
538
- const first = symbol?.getDeclarations()[0];
539
- if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
540
- const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
541
- const target = importDecl?.getModuleSpecifierSourceFile();
542
- if (target)
543
- return modulePath(ctx.rootDir, target.getFilePath());
544
- }
545
- const decl = resolveDeclaration(id)[0];
1887
+ const decl = resolveDeclaration(id, ctx)[0];
546
1888
  if (decl)
547
- return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1889
+ return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
1890
+ return;
1891
+ }
1892
+ function importModuleOf(id, ctx) {
1893
+ const symbol = ctx.checker.getSymbolAtLocation(id);
1894
+ const declarations = symbol?.declarations ?? [];
1895
+ for (const declaration of declarations) {
1896
+ let current = declaration;
1897
+ while (current) {
1898
+ if (ts3.isImportDeclaration(current)) {
1899
+ const moduleSpecifier = current.moduleSpecifier;
1900
+ return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
1901
+ }
1902
+ current = current.parent;
1903
+ }
1904
+ }
548
1905
  return;
549
1906
  }
550
1907
  function findDecorator(cls, name) {
551
- return cls.getDecorators().find((dec) => decoratorName(dec) === name);
1908
+ return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
552
1909
  }
553
- function decoratorName(dec) {
554
- const expr = dec.getExpression();
555
- if (Node.isCallExpression(expr)) {
556
- return expr.getExpression().getText().split(".").pop();
1910
+ function decoratorName2(dec) {
1911
+ const expr = dec.expression;
1912
+ if (ts3.isCallExpression(expr)) {
1913
+ return nodeText(expr.expression).split(".").pop();
1914
+ }
1915
+ if (ts3.isIdentifier(expr))
1916
+ return expr.text;
1917
+ return;
1918
+ }
1919
+ function decoratorObjectArg(dec) {
1920
+ const expr = dec.expression;
1921
+ if (!ts3.isCallExpression(expr))
1922
+ return;
1923
+ const arg = expr.arguments[0];
1924
+ return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
1925
+ }
1926
+ function getProp(obj, name) {
1927
+ const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
1928
+ if (!prop)
1929
+ return;
1930
+ if (ts3.isPropertyAssignment(prop))
1931
+ return prop.initializer;
1932
+ if (ts3.isShorthandPropertyAssignment(prop))
1933
+ return prop.name;
1934
+ return;
1935
+ }
1936
+ function toCompilerDiagnostic(diagnostic, rootDir) {
1937
+ const file = diagnostic.file;
1938
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
1939
+ return {
1940
+ severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
1941
+ code: `typescript-${diagnostic.code}`,
1942
+ errorCode: `TS${diagnostic.code}`,
1943
+ message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
1944
+ `),
1945
+ file: file ? sourcePath(rootDir, file.fileName) : undefined,
1946
+ line: position ? position.line + 1 : undefined
1947
+ };
1948
+ }
1949
+ function stringLiteralProp(obj, name) {
1950
+ const expr = getProp(obj, name);
1951
+ return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
1952
+ }
1953
+ function arrayProp(obj, name) {
1954
+ const expr = getProp(obj, name);
1955
+ return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
1956
+ }
1957
+ function parseAspectRefs(expression, ctx, owner) {
1958
+ if (!expression)
1959
+ return [];
1960
+ if (!ts3.isArrayLiteralExpression(expression)) {
1961
+ ctx.diagnostics.push({
1962
+ severity: "error",
1963
+ code: "dynamic-aspect-reference",
1964
+ message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
1965
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
1966
+ line: lineOf(expression),
1967
+ suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
1968
+ errorCode: "SC4010",
1969
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1970
+ });
1971
+ return [];
1972
+ }
1973
+ const refs = [];
1974
+ for (const element of expression.elements) {
1975
+ if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
1976
+ ctx.diagnostics.push({
1977
+ severity: "error",
1978
+ code: "dynamic-aspect-reference",
1979
+ message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
1980
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
1981
+ line: lineOf(element),
1982
+ suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
1983
+ errorCode: "SC4010",
1984
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1985
+ });
1986
+ continue;
1987
+ }
1988
+ const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
1989
+ if (!declaration) {
1990
+ ctx.diagnostics.push({
1991
+ severity: "error",
1992
+ code: "invalid-aspect-reference",
1993
+ message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
1994
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
1995
+ line: lineOf(element),
1996
+ suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
1997
+ errorCode: "SC4011",
1998
+ docsUrl: "https://supacloud.dev/errors/SC4011"
1999
+ });
2000
+ continue;
2001
+ }
2002
+ const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
2003
+ if (!name)
2004
+ continue;
2005
+ const declaredFile = declaration.getSourceFile().fileName;
2006
+ const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2007
+ refs.push({
2008
+ name,
2009
+ expression: element.text,
2010
+ importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
2011
+ importModule: projectLocal ? undefined : importModuleOf(element, ctx)
2012
+ });
557
2013
  }
558
- if (Node.isIdentifier(expr))
559
- return expr.getText();
560
- return;
2014
+ return refs;
561
2015
  }
562
- function decoratorObjectArg(dec) {
563
- const expr = dec.getExpression();
564
- if (!Node.isCallExpression(expr))
2016
+ function booleanProp(obj, name) {
2017
+ const expr = getProp(obj, name);
2018
+ if (!expr)
565
2019
  return;
566
- const arg = expr.getArguments()[0];
567
- return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
568
- }
569
- function getProp(obj, name) {
570
- const prop = obj.getProperty(name);
571
- if (prop && Node.isPropertyAssignment(prop))
572
- return prop.getInitializer();
2020
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
2021
+ return true;
2022
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
2023
+ return false;
573
2024
  return;
574
2025
  }
575
- function stringLiteralProp(obj, name) {
576
- const expr = getProp(obj, name);
577
- return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
578
- }
579
- function arrayProp(obj, name) {
580
- const expr = getProp(obj, name);
581
- return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
582
- }
583
2026
  function parseScopeProp(obj) {
584
2027
  const scope = stringLiteralProp(obj, "scope");
585
- return scope && SCOPES.includes(scope) ? scope : undefined;
2028
+ return scope && isScope(scope) ? scope : undefined;
2029
+ }
2030
+ function parseBindingOptions(args, defaultName) {
2031
+ let name = defaultName;
2032
+ let transform;
2033
+ let defaultValue;
2034
+ const first = args[0];
2035
+ const second = args[1];
2036
+ if (first && ts3.isStringLiteral(first)) {
2037
+ name = first.text;
2038
+ } else if (first && ts3.isObjectLiteralExpression(first)) {
2039
+ const nameProp = getProp(first, "name");
2040
+ if (nameProp && ts3.isStringLiteral(nameProp)) {
2041
+ name = nameProp.text;
2042
+ }
2043
+ const trProp = getProp(first, "transform");
2044
+ if (trProp && ts3.isStringLiteral(trProp)) {
2045
+ const val = trProp.text;
2046
+ if (val === "number" || val === "boolean" || val === "string") {
2047
+ transform = val;
2048
+ }
2049
+ }
2050
+ const defProp = getProp(first, "default");
2051
+ if (defProp) {
2052
+ defaultValue = parseLiteralValue(defProp);
2053
+ }
2054
+ }
2055
+ if (second && ts3.isObjectLiteralExpression(second)) {
2056
+ const trProp = getProp(second, "transform");
2057
+ if (trProp && ts3.isStringLiteral(trProp)) {
2058
+ const val = trProp.text;
2059
+ if (val === "number" || val === "boolean" || val === "string") {
2060
+ transform = val;
2061
+ }
2062
+ }
2063
+ const defProp = getProp(second, "default");
2064
+ if (defProp) {
2065
+ defaultValue = parseLiteralValue(defProp);
2066
+ }
2067
+ }
2068
+ return { name, transform, default: defaultValue };
2069
+ }
2070
+ function parseLiteralValue(node) {
2071
+ if (ts3.isStringLiteral(node))
2072
+ return node.text;
2073
+ if (ts3.isNumericLiteral(node))
2074
+ return Number(node.text);
2075
+ if (node.kind === ts3.SyntaxKind.TrueKeyword)
2076
+ return true;
2077
+ if (node.kind === ts3.SyntaxKind.FalseKeyword)
2078
+ return false;
2079
+ if (ts3.isArrayLiteralExpression(node)) {
2080
+ return node.elements.map(parseLiteralValue);
2081
+ }
2082
+ if (ts3.isObjectLiteralExpression(node)) {
2083
+ return parseObjectLiteralValues(node);
2084
+ }
2085
+ return;
2086
+ }
2087
+ function parseObjectLiteralValues(obj) {
2088
+ const result = {};
2089
+ for (const prop of obj.properties) {
2090
+ if (ts3.isPropertyAssignment(prop)) {
2091
+ const name = propertyName(prop.name);
2092
+ const init = prop.initializer;
2093
+ if (init) {
2094
+ result[name] = parseLiteralValue(init);
2095
+ }
2096
+ }
2097
+ }
2098
+ return result;
586
2099
  }
587
2100
  function modulePath(rootDir, absFile) {
588
2101
  return sourcePath(rootDir, absFile).replace(/\.(ts|tsx|js|mts|cts)$/, "");
@@ -594,7 +2107,8 @@ function warn(ctx, code, message, file, line) {
594
2107
  ctx.diagnostics.push({ severity: "warn", code, message, file, line });
595
2108
  }
596
2109
  // src/generate.ts
597
- import { mkdir, writeFile } from "node:fs/promises";
2110
+ import { createHash as createHash4 } from "node:crypto";
2111
+ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
598
2112
  import { join as join2 } from "node:path";
599
2113
 
600
2114
  // src/util.ts
@@ -627,6 +2141,28 @@ function isRequestContextToken(token, tokenNames) {
627
2141
  function isJobContextToken(token, tokenNames) {
628
2142
  return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
629
2143
  }
2144
+ function joinRoutePaths(prefix, path) {
2145
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
2146
+ const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
2147
+ return normalized;
2148
+ }
2149
+ function findClosestMatch(target, candidates) {
2150
+ if (candidates.length === 0)
2151
+ return;
2152
+ if (candidates.length === 1)
2153
+ return candidates[0];
2154
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2155
+ const targetNorm = norm(target);
2156
+ for (const c of candidates) {
2157
+ if (norm(c) === targetNorm)
2158
+ return c;
2159
+ }
2160
+ for (const c of candidates) {
2161
+ if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
2162
+ return c;
2163
+ }
2164
+ return candidates[0];
2165
+ }
630
2166
 
631
2167
  // src/generate.ts
632
2168
  var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
@@ -639,6 +2175,29 @@ var INTERFACES = `export interface CompiledRoute {
639
2175
  query?: unknown;
640
2176
  response?: unknown;
641
2177
  command?: string;
2178
+ guards?: string[];
2179
+ canMatch?: string[];
2180
+ canDeactivate?: string[];
2181
+ resolvers?: Record<string, string>;
2182
+ redirectTo?: string;
2183
+ pathMatch?: "full" | "prefix";
2184
+ paramTransforms?: Record<string, "number" | "boolean" | "string">;
2185
+ paramDefaults?: Record<string, unknown>;
2186
+ queryTransforms?: Record<string, "number" | "boolean" | "string">;
2187
+ queryDefaults?: Record<string, unknown>;
2188
+ title?: string;
2189
+ data?: Record<string, unknown>;
2190
+ aspects?: CompiledAspect[];
2191
+ invoker?: (
2192
+ controller: unknown,
2193
+ request: {
2194
+ params?: Record<string, unknown>;
2195
+ query?: Record<string, unknown>;
2196
+ body?: unknown;
2197
+ headers?: Record<string, unknown>;
2198
+ context?: unknown;
2199
+ },
2200
+ ) => Promise<unknown> | unknown;
642
2201
  }
643
2202
 
644
2203
  export interface CompiledCommand {
@@ -648,8 +2207,34 @@ export interface CompiledCommand {
648
2207
  transaction: "required" | "none";
649
2208
  audit?: string;
650
2209
  idempotency: "required" | "none";
2210
+ standalone?: boolean;
2211
+ aspects?: CompiledAspect[];
2212
+ }
2213
+
2214
+ export interface CompiledJob {
2215
+ className: string;
2216
+ name: string;
2217
+ serviceKey: string;
2218
+ scope: "application" | "request" | "job";
2219
+ aspects?: CompiledAspect[];
2220
+ }
2221
+
2222
+ export interface CompiledAspectContext {
2223
+ kind: "route" | "command" | "job";
2224
+ name: string;
2225
+ input: unknown;
2226
+ request?: Request;
2227
+ requestContext?: unknown;
2228
+ scope?: Record<string, unknown>;
2229
+ services?: Record<string, unknown>;
2230
+ metadata?: unknown;
651
2231
  }
652
2232
 
2233
+ export type CompiledAspect = (
2234
+ context: CompiledAspectContext,
2235
+ next: () => unknown | Promise<unknown>,
2236
+ ) => unknown | Promise<unknown>;
2237
+
653
2238
  export interface CompiledController {
654
2239
  path: string;
655
2240
  serviceKey: string;
@@ -668,16 +2253,102 @@ export interface CompiledModule {
668
2253
  ctx: unknown,
669
2254
  imported?: Record<string, Record<string, unknown>>,
670
2255
  ): Record<string, unknown>;
2256
+ destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
671
2257
  createJobScope?(
672
2258
  services: Record<string, unknown>,
673
2259
  ctx: unknown,
674
2260
  imported?: Record<string, Record<string, unknown>>,
675
2261
  ): Record<string, unknown>;
2262
+ destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
676
2263
  controllers: CompiledController[];
677
2264
  commands: CompiledCommand[];
2265
+ jobs: CompiledJob[];
2266
+ aspects?: CompiledAspect[];
2267
+ }`;
2268
+ var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
2269
+ return typeof value === "object" && value !== null;
2270
+ }
2271
+
2272
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2273
+ return typeof value === "function";
2274
+ }
2275
+
2276
+ function resolveFactoryValue(value: unknown): unknown {
2277
+ if (!isRecord(value) || !isFunction(value.factory)) return undefined;
2278
+ return value.factory();
2279
+ }
2280
+
2281
+ const scopeDestructions = new WeakMap<object, Promise<void>>();
2282
+
2283
+ function destroyScopeInstances(
2284
+ scope: Record<string, unknown>,
2285
+ plan: readonly { key: string; index?: number }[],
2286
+ ): Promise<void> {
2287
+ const pending = scopeDestructions.get(scope);
2288
+ if (pending) return pending;
2289
+ const destruction = Promise.resolve().then(async () => {
2290
+ const errors: unknown[] = [];
2291
+ const seen = new Set<unknown>();
2292
+ for (const entry of [...plan].reverse()) {
2293
+ const value = scope[entry.key];
2294
+ const instance = entry.index === undefined ? value
2295
+ : Array.isArray(value) ? value[entry.index] : undefined;
2296
+ if (seen.has(instance)) continue;
2297
+ seen.add(instance);
2298
+ try {
2299
+ if (isRecord(instance) && isFunction(instance.onDestroy)) {
2300
+ await instance.onDestroy();
2301
+ } else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
2302
+ await instance.ngOnDestroy();
2303
+ }
2304
+ } catch (error) {
2305
+ errors.push(error);
2306
+ }
2307
+ }
2308
+ if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
2309
+ });
2310
+ scopeDestructions.set(scope, destruction);
2311
+ return destruction;
678
2312
  }`;
679
2313
  function renderApplication(graph, options) {
680
- const modules = topoSortModules(graph.modules);
2314
+ let modules = topoSortModules(graph.modules);
2315
+ if (options.treeShakeUnusedProviders) {
2316
+ const referencedTokens = new Set;
2317
+ for (const mod of graph.modules) {
2318
+ for (const exp of mod.exports)
2319
+ referencedTokens.add(exp);
2320
+ for (const ctrl of mod.controllers) {
2321
+ for (const d of ctrl.deps)
2322
+ referencedTokens.add(d);
2323
+ for (const d of ctrl.optionalDeps ?? [])
2324
+ referencedTokens.add(d);
2325
+ for (const d of ctrl.selfDeps ?? [])
2326
+ referencedTokens.add(d);
2327
+ for (const d of ctrl.skipSelfDeps ?? [])
2328
+ referencedTokens.add(d);
2329
+ for (const d of ctrl.hostDeps ?? [])
2330
+ referencedTokens.add(d);
2331
+ }
2332
+ for (const p of mod.providers) {
2333
+ for (const d of p.deps ?? [])
2334
+ referencedTokens.add(d);
2335
+ for (const d of p.optionalDeps ?? [])
2336
+ referencedTokens.add(d);
2337
+ for (const d of p.selfDeps ?? [])
2338
+ referencedTokens.add(d);
2339
+ for (const d of p.skipSelfDeps ?? [])
2340
+ referencedTokens.add(d);
2341
+ for (const d of p.hostDeps ?? [])
2342
+ referencedTokens.add(d);
2343
+ if (p.useExisting)
2344
+ referencedTokens.add(p.useExisting);
2345
+ }
2346
+ }
2347
+ modules = modules.map((mod) => ({
2348
+ ...mod,
2349
+ providers: mod.providers.filter((p) => p.providedIn !== "root" || p.multi || referencedTokens.has(p.token) || p.exported)
2350
+ }));
2351
+ }
681
2352
  const imports = new ImportManager;
682
2353
  const factorySections = [];
683
2354
  const descriptorEntries = [];
@@ -693,12 +2364,46 @@ function renderApplication(graph, options) {
693
2364
  ...imports.size > 0 ? [""] : [],
694
2365
  INTERFACES,
695
2366
  "",
2367
+ TYPE_GUARDS,
2368
+ "",
696
2369
  "export function createCompiledModules(): CompiledModule[] {",
697
2370
  " return [",
698
2371
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
699
2372
  " ];",
700
2373
  "}",
701
2374
  "",
2375
+ "export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
2376
+ ' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
2377
+ " for (const group of initializers) {",
2378
+ " if (Array.isArray(group)) {",
2379
+ " for (const init of group) {",
2380
+ " if (isFunction(init)) await init();",
2381
+ " }",
2382
+ " } else if (isFunction(group)) {",
2383
+ " await group();",
2384
+ " }",
2385
+ " }",
2386
+ "}",
2387
+ "",
2388
+ "export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
2389
+ ' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
2390
+ " if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
2391
+ " await destroyRef.destroy();",
2392
+ " } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
2393
+ " for (const teardown of [...destroyRef._teardowns].reverse()) {",
2394
+ " if (isFunction(teardown)) await teardown();",
2395
+ " }",
2396
+ " }",
2397
+ " const instances = Object.values(services);",
2398
+ " for (const inst of instances.reverse()) {",
2399
+ " if (isRecord(inst) && isFunction(inst.onDestroy)) {",
2400
+ " await inst.onDestroy();",
2401
+ " } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
2402
+ " await inst.ngOnDestroy();",
2403
+ " }",
2404
+ " }",
2405
+ "}",
2406
+ "",
702
2407
  ...factorySections,
703
2408
  ""
704
2409
  ].join(`
@@ -708,10 +2413,14 @@ function renderApplication(graph, options) {
708
2413
  modules: graph.modules,
709
2414
  externalTokens: graph.externalTokens
710
2415
  };
2416
+ const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
2417
+ const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
711
2418
  return {
712
2419
  applicationCode: code,
713
2420
  manifestJson: JSON.stringify(manifest, null, 2) + `
714
- `
2421
+ `,
2422
+ clientCode,
2423
+ permissionsCode
715
2424
  };
716
2425
  }
717
2426
  async function generateApplication(graph, options) {
@@ -719,9 +2428,50 @@ async function generateApplication(graph, options) {
719
2428
  await mkdir(options.outDir, { recursive: true });
720
2429
  const applicationPath = join2(options.outDir, "application.ts");
721
2430
  const manifestPath = join2(options.outDir, "app.manifest.json");
722
- await writeFile(applicationPath, rendered.applicationCode, "utf8");
723
- await writeFile(manifestPath, rendered.manifestJson, "utf8");
724
- return [applicationPath, manifestPath];
2431
+ const written = [];
2432
+ if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
2433
+ written.push(applicationPath);
2434
+ }
2435
+ if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
2436
+ written.push(manifestPath);
2437
+ }
2438
+ if (rendered.clientCode) {
2439
+ const clientPath = join2(options.outDir, "client.ts");
2440
+ if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
2441
+ written.push(clientPath);
2442
+ }
2443
+ }
2444
+ if (rendered.permissionsCode) {
2445
+ const permissionsPath = join2(options.outDir, "permissions.ts");
2446
+ if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
2447
+ written.push(permissionsPath);
2448
+ }
2449
+ }
2450
+ return written;
2451
+ }
2452
+ async function writeFileIfChanged(path, content, hashes) {
2453
+ const hash = createHash4("sha1").update(content).digest("hex");
2454
+ if (hashes?.get(path) === hash) {
2455
+ try {
2456
+ await access(path);
2457
+ return false;
2458
+ } catch {}
2459
+ }
2460
+ await writeFileAtomic(path, content);
2461
+ hashes?.set(path, hash);
2462
+ return true;
2463
+ }
2464
+ async function writeFileAtomic(path, content) {
2465
+ const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2466
+ try {
2467
+ await writeFile(temporaryPath, content, "utf8");
2468
+ await rename(temporaryPath, path);
2469
+ } catch (error) {
2470
+ await unlink(temporaryPath).catch(() => {
2471
+ return;
2472
+ });
2473
+ throw error;
2474
+ }
725
2475
  }
726
2476
  function factoryOfScope(scope) {
727
2477
  return scope === "application" ? "services" : scope;
@@ -761,11 +2511,13 @@ class ImportManager {
761
2511
  get size() {
762
2512
  return this.entries.size;
763
2513
  }
764
- add(exported, importPath) {
765
- if (!importPath)
2514
+ add(exported, importPath, importModule) {
2515
+ const path = importModule ?? importPath;
2516
+ const packageImport = importModule !== undefined;
2517
+ if (!path)
766
2518
  return exported;
767
2519
  for (const [local2, entry] of this.entries) {
768
- if (entry.path === importPath && entry.exported === exported)
2520
+ if (entry.path === path && entry.exported === exported && entry.package === packageImport)
769
2521
  return local2;
770
2522
  }
771
2523
  let local = exported;
@@ -774,18 +2526,18 @@ class ImportManager {
774
2526
  local = `${exported}${counter}`;
775
2527
  counter += 1;
776
2528
  }
777
- this.entries.set(local, { path: importPath, exported });
2529
+ this.entries.set(local, { path, exported, package: packageImport });
778
2530
  return local;
779
2531
  }
780
2532
  render(rootDir, outDir) {
781
2533
  const byPath = new Map;
782
2534
  for (const [local, entry] of this.entries) {
783
- const list = byPath.get(entry.path) ?? [];
2535
+ const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
2536
+ const list = byPath.get(spec) ?? [];
784
2537
  list.push({ exported: entry.exported, local });
785
- byPath.set(entry.path, list);
2538
+ byPath.set(spec, list);
786
2539
  }
787
- return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
788
- const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
2540
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
789
2541
  const names = symbols.sort((a, b) => a.exported.localeCompare(b.exported)).map((s) => s.local === s.exported ? s.exported : `${s.exported} as ${s.local}`).join(", ");
790
2542
  return `import { ${names} } from "${spec}";`;
791
2543
  });
@@ -807,14 +2559,19 @@ class ModuleGenerator {
807
2559
  this.module = module;
808
2560
  this.imports = imports;
809
2561
  this.pascal = pascalName(module.name);
2562
+ if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
2563
+ imports.add("runInInjectionContext", undefined, "@supacloud/app");
2564
+ }
810
2565
  }
811
2566
  renderFactories() {
812
2567
  const sections = [this.renderServicesFactory()];
813
2568
  if (this.hasFactoryContent("request")) {
814
2569
  sections.push(this.renderScopeFactory("request"));
2570
+ sections.push(this.renderScopeDestroyer("request"));
815
2571
  }
816
2572
  if (this.hasFactoryContent("job")) {
817
2573
  sections.push(this.renderScopeFactory("job"));
2574
+ sections.push(this.renderScopeDestroyer("job"));
818
2575
  }
819
2576
  return sections;
820
2577
  }
@@ -826,12 +2583,18 @@ class ModuleGenerator {
826
2583
  ];
827
2584
  if (this.hasFactoryContent("request")) {
828
2585
  lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
2586
+ lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
829
2587
  }
830
2588
  if (this.hasFactoryContent("job")) {
831
2589
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
2590
+ lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
832
2591
  }
833
2592
  lines.push(` controllers: ${this.renderControllers()},`);
834
- lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
2593
+ lines.push(` commands: ${this.renderCommands()},`);
2594
+ lines.push(` jobs: ${this.renderJobs()},`);
2595
+ if (this.module.aspects && this.module.aspects.length > 0) {
2596
+ lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
2597
+ }
835
2598
  lines.push(`}`);
836
2599
  return lines.join(`
837
2600
  `);
@@ -858,6 +2621,84 @@ class ModuleGenerator {
858
2621
  }
859
2622
  if (route.command)
860
2623
  fields.push(`command: ${JSON.stringify(route.command)}`);
2624
+ if (route.guards && route.guards.length > 0) {
2625
+ fields.push(`guards: ${JSON.stringify(route.guards)}`);
2626
+ }
2627
+ if (route.canMatch && route.canMatch.length > 0) {
2628
+ fields.push(`canMatch: ${JSON.stringify(route.canMatch)}`);
2629
+ }
2630
+ if (route.canDeactivate && route.canDeactivate.length > 0) {
2631
+ fields.push(`canDeactivate: ${JSON.stringify(route.canDeactivate)}`);
2632
+ }
2633
+ if (route.resolvers && Object.keys(route.resolvers).length > 0) {
2634
+ fields.push(`resolvers: ${JSON.stringify(route.resolvers)}`);
2635
+ }
2636
+ if (route.redirectTo) {
2637
+ fields.push(`redirectTo: ${JSON.stringify(route.redirectTo)}`);
2638
+ }
2639
+ if (route.pathMatch) {
2640
+ fields.push(`pathMatch: ${JSON.stringify(route.pathMatch)}`);
2641
+ }
2642
+ if (route.paramTransforms && Object.keys(route.paramTransforms).length > 0) {
2643
+ fields.push(`paramTransforms: ${JSON.stringify(route.paramTransforms)}`);
2644
+ }
2645
+ if (route.paramDefaults && Object.keys(route.paramDefaults).length > 0) {
2646
+ fields.push(`paramDefaults: ${JSON.stringify(route.paramDefaults)}`);
2647
+ }
2648
+ if (route.queryTransforms && Object.keys(route.queryTransforms).length > 0) {
2649
+ fields.push(`queryTransforms: ${JSON.stringify(route.queryTransforms)}`);
2650
+ }
2651
+ if (route.queryDefaults && Object.keys(route.queryDefaults).length > 0) {
2652
+ fields.push(`queryDefaults: ${JSON.stringify(route.queryDefaults)}`);
2653
+ }
2654
+ if (route.title) {
2655
+ fields.push(`title: ${JSON.stringify(route.title)}`);
2656
+ }
2657
+ if (route.data && Object.keys(route.data).length > 0) {
2658
+ fields.push(`data: ${JSON.stringify(route.data)}`);
2659
+ }
2660
+ if (route.aspects && route.aspects.length > 0) {
2661
+ fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2662
+ }
2663
+ const invokerArgs = (route.handlerParams ?? []).map((hp) => {
2664
+ if (hp.kind === "param") {
2665
+ const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
2666
+ const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
2667
+ if (hp.transform === "number") {
2668
+ return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
2669
+ }
2670
+ if (hp.transform === "boolean") {
2671
+ return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
2672
+ }
2673
+ if (hp.transform === "string") {
2674
+ return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
2675
+ }
2676
+ return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
2677
+ }
2678
+ if (hp.kind === "query") {
2679
+ const accessor = `req.query?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
2680
+ const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
2681
+ if (hp.transform === "number") {
2682
+ return `(${accessor} !== undefined ? Number(${accessor}) : ${fallback})`;
2683
+ }
2684
+ if (hp.transform === "boolean") {
2685
+ return `(${accessor} !== undefined ? Boolean(${accessor}) : ${fallback})`;
2686
+ }
2687
+ if (hp.transform === "string") {
2688
+ return `(${accessor} !== undefined ? String(${accessor}) : ${fallback})`;
2689
+ }
2690
+ return `(${accessor} !== undefined ? ${accessor} : ${fallback})`;
2691
+ }
2692
+ if (hp.kind === "body")
2693
+ return "req.body";
2694
+ if (hp.kind === "headers")
2695
+ return "req.headers";
2696
+ if (hp.kind === "context")
2697
+ return "(req.context ?? req)";
2698
+ return "undefined";
2699
+ });
2700
+ const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
2701
+ fields.push(`invoker: async (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (typeof handler !== "function") throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return await Reflect.apply(handler, ctrl, [${callArgs}]); }`);
861
2702
  return `{ ${fields.join(", ")} }`;
862
2703
  });
863
2704
  return [
@@ -874,6 +2715,32 @@ class ModuleGenerator {
874
2715
  ${indent(item, 2)}`).join(",")}
875
2716
  ]`;
876
2717
  }
2718
+ renderCommands() {
2719
+ if (this.module.commands.length === 0)
2720
+ return "[]";
2721
+ return `[${this.module.commands.map((command) => {
2722
+ const fields = [
2723
+ `className: ${JSON.stringify(command.className)}`,
2724
+ `name: ${JSON.stringify(command.name)}`,
2725
+ `permission: ${JSON.stringify(command.permission ?? "")}`,
2726
+ `transaction: ${JSON.stringify(command.transaction)}`,
2727
+ ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2728
+ `idempotency: ${JSON.stringify(command.idempotency)}`,
2729
+ ...command.standalone ? ["standalone: true"] : [],
2730
+ ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2731
+ ];
2732
+ return `{ ${fields.join(", ")} }`;
2733
+ }).join(", ")}]`;
2734
+ }
2735
+ renderJobs() {
2736
+ const jobs = this.module.jobs ?? [];
2737
+ if (jobs.length === 0)
2738
+ return "[]";
2739
+ return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(camelName(job.className))}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
2740
+ }
2741
+ renderAspects(aspects) {
2742
+ return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
2743
+ }
877
2744
  renderServicesFactory() {
878
2745
  return [
879
2746
  `function create${this.pascal}Services(`,
@@ -896,6 +2763,30 @@ ${indent(item, 2)}`).join(",")}
896
2763
  indent(this.renderFactoryBody(kind), 2),
897
2764
  `}`
898
2765
  ].join(`
2766
+ `);
2767
+ }
2768
+ renderScopeDestroyer(kind) {
2769
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
2770
+ const plan = [];
2771
+ const multiIndices = new Map;
2772
+ for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
2773
+ const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
2774
+ if (index !== undefined)
2775
+ multiIndices.set(provider.token, index + 1);
2776
+ if (provider.kind !== "existing" && provider.hasOnDestroy) {
2777
+ plan.push({ key: camelName(provider.token), index });
2778
+ }
2779
+ }
2780
+ for (const controller of this.module.controllers) {
2781
+ if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
2782
+ plan.push({ key: camelName(controller.className) });
2783
+ }
2784
+ }
2785
+ return [
2786
+ `async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
2787
+ ` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
2788
+ `}`
2789
+ ].join(`
899
2790
  `);
900
2791
  }
901
2792
  renderFactoryBody(kind) {
@@ -903,11 +2794,24 @@ ${indent(item, 2)}`).join(",")}
903
2794
  const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
904
2795
  const lines = [];
905
2796
  const returns = new Map;
2797
+ const multiGroups = new Map;
906
2798
  for (const provider of providers) {
907
- const emitted = this.emitProvider(provider, kind);
908
- if (emitted.constLine)
909
- lines.push(emitted.constLine);
910
- returns.set(emitted.key, emitted.expr);
2799
+ if (provider.multi) {
2800
+ const emitted = this.emitProvider(provider, kind, true);
2801
+ if (emitted.constLine)
2802
+ lines.push(emitted.constLine);
2803
+ const list = multiGroups.get(emitted.key) ?? [];
2804
+ list.push(emitted.expr);
2805
+ multiGroups.set(emitted.key, list);
2806
+ } else {
2807
+ const emitted = this.emitProvider(provider, kind, false);
2808
+ if (emitted.constLine)
2809
+ lines.push(emitted.constLine);
2810
+ returns.set(emitted.key, emitted.expr);
2811
+ }
2812
+ }
2813
+ for (const [key, exprs] of multiGroups) {
2814
+ returns.set(key, `[${exprs.join(", ")}]`);
911
2815
  }
912
2816
  for (const controller of controllers) {
913
2817
  const emitted = this.emitController(controller, kind);
@@ -919,37 +2823,80 @@ ${indent(item, 2)}`).join(",")}
919
2823
  return lines.join(`
920
2824
  `);
921
2825
  }
922
- emitProvider(provider, kind) {
2826
+ emitProvider(provider, kind, isMulti = false) {
923
2827
  const key = camelName(provider.token);
924
2828
  switch (provider.kind) {
925
2829
  case "class": {
926
- const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
927
- const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
928
- const local = this.localVar(provider.token, kind);
929
- return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
2830
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
2831
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
2832
+ const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
2833
+ return {
2834
+ constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
2835
+ key,
2836
+ expr: local
2837
+ };
930
2838
  }
931
2839
  case "value": {
932
- const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
933
- const local = this.localVar(provider.token, kind);
2840
+ const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
2841
+ const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
934
2842
  return { constLine: `const ${local} = ${expr};`, key, expr: local };
935
2843
  }
936
2844
  case "factory": {
937
- const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
938
- const args = provider.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
939
- const local = this.localVar(provider.token, kind);
2845
+ if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
2846
+ const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
2847
+ const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
2848
+ const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
2849
+ return { constLine, key, expr: local2 };
2850
+ }
2851
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2852
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
2853
+ const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
940
2854
  return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
941
2855
  }
942
2856
  case "existing": {
943
- return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind) };
2857
+ return {
2858
+ key,
2859
+ expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
2860
+ };
944
2861
  }
945
2862
  }
946
2863
  }
947
2864
  emitController(controller, kind) {
948
2865
  const className = this.imports.add(controller.className, controller.importPath);
949
- const args = controller.deps.map((dep) => this.depExpr(dep, kind)).join(", ");
2866
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
950
2867
  const key = camelName(controller.className);
951
2868
  const local = this.localVar(controller.className, kind);
952
- return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
2869
+ return {
2870
+ constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
2871
+ key,
2872
+ expr: local
2873
+ };
2874
+ }
2875
+ instantiate(className, args, kind, functionalInjects) {
2876
+ if (!functionalInjects || functionalInjects.length === 0) {
2877
+ return `new ${className}(${args})`;
2878
+ }
2879
+ const clauses = functionalInjects.map((entry) => {
2880
+ const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
2881
+ const value = this.depExpr(entry.token, kind, {
2882
+ optional: entry.optional,
2883
+ self: entry.self,
2884
+ skipSelf: entry.skipSelf,
2885
+ host: entry.host
2886
+ });
2887
+ return `if (token === ${token}) return ${value} as T;`;
2888
+ });
2889
+ const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
2890
+ const injector = [
2891
+ `{`,
2892
+ `get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
2893
+ ...clauses,
2894
+ missing,
2895
+ `},`,
2896
+ `}`
2897
+ ].join(`
2898
+ `);
2899
+ return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
953
2900
  }
954
2901
  localVar(token, kind) {
955
2902
  const locals = this.locals[kind];
@@ -966,23 +2913,34 @@ ${indent(item, 2)}`).join(",")}
966
2913
  locals.set(token, local);
967
2914
  return local;
968
2915
  }
969
- depExpr(token, kind) {
2916
+ depOptions(node, token) {
2917
+ return {
2918
+ optional: node.optionalDeps?.includes(token) ?? false,
2919
+ self: node.selfDeps?.includes(token) ?? false,
2920
+ skipSelf: node.skipSelfDeps?.includes(token) ?? false,
2921
+ host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
2922
+ };
2923
+ }
2924
+ depExpr(token, kind, options = {}) {
2925
+ const isOptional = options.optional ?? false;
2926
+ const isSelf = options.self ?? false;
2927
+ const isSkipSelf = options.skipSelf ?? false;
970
2928
  if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
971
2929
  return "ctx";
972
2930
  if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
973
2931
  return "ctx";
974
2932
  const own = this.module.providers.find((p) => p.token === token);
975
- if (own) {
2933
+ const ownIsLocal = own && factoryOfScope(own.scope) === kind;
2934
+ if (own && ownIsLocal && !isSkipSelf) {
976
2935
  if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
977
2936
  return this.locals[kind].get(token) ?? camelName(token);
978
2937
  }
979
- if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
980
- return this.depExpr(own.useExisting ?? token, kind);
981
- }
982
- if (kind === "services") {
983
- return `services.${camelName(token)}`;
2938
+ if (own.kind === "existing") {
2939
+ return this.depExpr(own.useExisting ?? token, kind, options);
984
2940
  }
985
- return `services.${camelName(token)}`;
2941
+ }
2942
+ if (isSelf) {
2943
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
986
2944
  }
987
2945
  for (const importName of this.module.imports) {
988
2946
  const imported = this.graph.modules.find((m) => m.name === importName);
@@ -992,9 +2950,20 @@ ${indent(item, 2)}`).join(",")}
992
2950
  return `imported.${importName}.${camelName(token)}`;
993
2951
  return `imported.${importName}.${camelName(token)}`;
994
2952
  }
2953
+ for (const mod of this.graph.modules) {
2954
+ const rootProv = mod.providers.find((p) => p.token === token && p.providedIn === "root");
2955
+ if (rootProv) {
2956
+ return `imported.${mod.name}.${camelName(token)}`;
2957
+ }
2958
+ }
2959
+ if (isOptional && !this.graph.externalTokens.includes(token)) {
2960
+ return "undefined";
2961
+ }
2962
+ if (isSelf)
2963
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
995
2964
  if (kind === "services")
996
- return `deps.${camelName(token)}`;
997
- return `services.${camelName(token)}`;
2965
+ return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
2966
+ return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
998
2967
  }
999
2968
  }
1000
2969
  function orderProviders(providers) {
@@ -1016,6 +2985,226 @@ function orderProviders(providers) {
1016
2985
  }
1017
2986
  return result;
1018
2987
  }
2988
+ function renderClient(graph, _options) {
2989
+ const controllerEntries = [];
2990
+ const allRoutes = [];
2991
+ for (const module of graph.modules) {
2992
+ for (const controller of module.controllers) {
2993
+ const controllerKey = camelName(controller.className.replace(/Controller$/, ""));
2994
+ const routeMethods = [];
2995
+ for (const route of controller.routes) {
2996
+ const fullPath = joinRoutePaths(controller.path, route.path);
2997
+ allRoutes.push({
2998
+ method: route.method,
2999
+ path: fullPath,
3000
+ controller: controller.className,
3001
+ handler: route.handler,
3002
+ command: route.command,
3003
+ guards: route.guards,
3004
+ canMatch: route.canMatch,
3005
+ canDeactivate: route.canDeactivate,
3006
+ resolvers: route.resolvers,
3007
+ redirectTo: route.redirectTo,
3008
+ pathMatch: route.pathMatch,
3009
+ paramTransforms: route.paramTransforms,
3010
+ paramDefaults: route.paramDefaults,
3011
+ queryTransforms: route.queryTransforms,
3012
+ queryDefaults: route.queryDefaults,
3013
+ title: route.title,
3014
+ data: route.data
3015
+ });
3016
+ routeMethods.push(`
3017
+ ${route.handler}: (options: {
3018
+ 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>"};
3019
+ query?: Record<string, unknown>;
3020
+ body?: unknown;
3021
+ headers?: Record<string, string>;
3022
+ }${route.pathParams && route.pathParams.length > 0 || (fullPath.match(/:([a-zA-Z0-9_]+)/g) ?? []).length > 0 ? "" : " = {}"}) => request(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, options),`);
3023
+ }
3024
+ controllerEntries.push(`
3025
+ ${controllerKey}: {${routeMethods.join("")}
3026
+ },`);
3027
+ }
3028
+ }
3029
+ return [
3030
+ HEADER,
3031
+ "",
3032
+ "export interface ClientRequestOptions {",
3033
+ " params?: Record<string, string | number>;",
3034
+ " query?: Record<string, unknown>;",
3035
+ " body?: unknown;",
3036
+ " headers?: Record<string, string>;",
3037
+ "}",
3038
+ "",
3039
+ "export type HttpInterceptorFn = (",
3040
+ " req: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
3041
+ " next: (req: { method: string; url: string; headers: Record<string, string>; body?: unknown }) => Promise<Response>,",
3042
+ ") => Promise<Response>;",
3043
+ "",
3044
+ "export interface ApiClientConfig {",
3045
+ " baseUrl?: string;",
3046
+ " fetch?: typeof fetch;",
3047
+ " headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);",
3048
+ " interceptors?: HttpInterceptorFn[];",
3049
+ "}",
3050
+ "",
3051
+ "export const API_ROUTES = " + JSON.stringify(allRoutes, null, 2) + " as const;",
3052
+ "",
3053
+ "export type AppRoutePath = typeof API_ROUTES[number]['path'];",
3054
+ "",
3055
+ "/**",
3056
+ " * Type-safe URL builder replacing route path parameters and appending query parameters.",
3057
+ " */",
3058
+ "export function buildRouteUrl(",
3059
+ " path: string,",
3060
+ " params?: Record<string, string | number>,",
3061
+ " query?: Record<string, unknown>,",
3062
+ "): string {",
3063
+ " let url = path;",
3064
+ " if (params) {",
3065
+ " for (const [key, value] of Object.entries(params)) {",
3066
+ " url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
3067
+ " }",
3068
+ " }",
3069
+ " if (query) {",
3070
+ " const searchParams = new URLSearchParams();",
3071
+ " for (const [k, v] of Object.entries(query)) {",
3072
+ " if (v !== undefined && v !== null) searchParams.set(k, String(v));",
3073
+ " }",
3074
+ " const qs = searchParams.toString();",
3075
+ ' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
3076
+ " }",
3077
+ " return url;",
3078
+ "}",
3079
+ "",
3080
+ "export function createApiClient(config: ApiClientConfig = {}) {",
3081
+ " const fetcher = config.fetch ?? globalThis.fetch.bind(globalThis);",
3082
+ ' const baseUrl = (config.baseUrl ?? "").replace(/\\/+$/, "");',
3083
+ "",
3084
+ " async function request<T = unknown>(",
3085
+ " method: string,",
3086
+ " path: string,",
3087
+ " options: ClientRequestOptions = {},",
3088
+ " ): Promise<T> {",
3089
+ " let url = `${baseUrl}${path}`;",
3090
+ " if (options.params) {",
3091
+ " for (const [key, value] of Object.entries(options.params)) {",
3092
+ " url = url.replace(`:${key}`, encodeURIComponent(String(value)));",
3093
+ " }",
3094
+ " }",
3095
+ " if (options.query) {",
3096
+ " const searchParams = new URLSearchParams();",
3097
+ " for (const [k, v] of Object.entries(options.query)) {",
3098
+ " if (v !== undefined && v !== null) searchParams.set(k, String(v));",
3099
+ " }",
3100
+ " const qs = searchParams.toString();",
3101
+ ' if (qs) url += (url.includes("?") ? "&" : "?") + qs;',
3102
+ " }",
3103
+ ' const customHeaders = typeof config.headers === "function" ? await config.headers() : config.headers;',
3104
+ " const headers: Record<string, string> = {",
3105
+ ' "content-type": "application/json",',
3106
+ " ...customHeaders,",
3107
+ " ...options.headers,",
3108
+ " };",
3109
+ " const interceptors = config.interceptors ?? [];",
3110
+ " const executeChain = (",
3111
+ " index: number,",
3112
+ " reqPayload: { method: string; url: string; headers: Record<string, string>; body?: unknown },",
3113
+ " ): Promise<Response> => {",
3114
+ " if (index < interceptors.length) {",
3115
+ " return interceptors[index](reqPayload, (nextPayload) => executeChain(index + 1, nextPayload));",
3116
+ " }",
3117
+ " return fetcher(reqPayload.url, {",
3118
+ " method: reqPayload.method,",
3119
+ " headers: reqPayload.headers,",
3120
+ " body: reqPayload.body !== undefined ? JSON.stringify(reqPayload.body) : undefined,",
3121
+ " });",
3122
+ " };",
3123
+ " const response = await executeChain(0, { method, url, headers, body: options.body });",
3124
+ " if (!response.ok) {",
3125
+ " const errBody = await response.text();",
3126
+ " throw new Error(`API request failed: ${method} ${path} -> ${response.status} ${errBody}`);",
3127
+ " }",
3128
+ ' const contentType = response.headers?.get("content-type") ?? "";',
3129
+ ' if (contentType.includes("application/json")) {',
3130
+ " return response.json() as Promise<T>;",
3131
+ " }",
3132
+ " return response.text() as Promise<T>;",
3133
+ " }",
3134
+ "",
3135
+ " return {",
3136
+ " request,",
3137
+ " buildRouteUrl,",
3138
+ " routes: API_ROUTES,",
3139
+ ...controllerEntries,
3140
+ " };",
3141
+ "}",
3142
+ "",
3143
+ "export type ApiClient = ReturnType<typeof createApiClient>;",
3144
+ ""
3145
+ ].join(`
3146
+ `);
3147
+ }
3148
+ function renderPermissions(graph) {
3149
+ const permissions = new Set;
3150
+ const bindings = [];
3151
+ for (const module of graph.modules) {
3152
+ for (const command of module.commands) {
3153
+ if (command.permission)
3154
+ permissions.add(command.permission);
3155
+ }
3156
+ for (const controller of module.controllers) {
3157
+ for (const route of controller.routes) {
3158
+ let perm;
3159
+ if (route.command) {
3160
+ const cmd = module.commands.find((c) => c.className === route.command);
3161
+ perm = cmd?.permission;
3162
+ }
3163
+ if (perm)
3164
+ permissions.add(perm);
3165
+ bindings.push({
3166
+ method: route.method,
3167
+ path: joinRoutePaths(controller.path, route.path),
3168
+ controller: controller.className,
3169
+ handler: route.handler,
3170
+ command: route.command,
3171
+ permission: perm
3172
+ });
3173
+ }
3174
+ }
3175
+ }
3176
+ const sortedPerms = [...permissions].sort();
3177
+ const enumEntries = sortedPerms.map((perm) => {
3178
+ const key = pascalName(perm.replace(/[^A-Za-z0-9]+/g, " "));
3179
+ return ` ${key}: ${JSON.stringify(perm)},`;
3180
+ });
3181
+ return [
3182
+ HEADER,
3183
+ "",
3184
+ "export const AppPermissions = {",
3185
+ ...enumEntries,
3186
+ "} as const;",
3187
+ "",
3188
+ "export type AppPermission = (typeof AppPermissions)[keyof typeof AppPermissions];",
3189
+ "",
3190
+ "export interface RoutePermissionBinding {",
3191
+ " method: string;",
3192
+ " path: string;",
3193
+ " controller: string;",
3194
+ " handler: string;",
3195
+ " command?: string;",
3196
+ " permission?: string;",
3197
+ "}",
3198
+ "",
3199
+ "export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
3200
+ "",
3201
+ "export function hasPermission(granted: string[], required: AppPermission | string): boolean {",
3202
+ ' return granted.includes("*") || granted.includes(required);',
3203
+ "}",
3204
+ ""
3205
+ ].join(`
3206
+ `);
3207
+ }
1019
3208
 
1020
3209
  // src/profiles.ts
1021
3210
  var MODULAR_MONOLITH_RULES = [
@@ -1190,6 +3379,57 @@ var SCOPE_LIFETIME_RANK = {
1190
3379
  request: 1,
1191
3380
  job: 1
1192
3381
  };
3382
+ var COMPILER_DIAGNOSTIC_CODES = {
3383
+ "circular-dependency": { code: "SC1001", docsUrl: "https://supacloud.dev/errors/SC1001" },
3384
+ "scope-violation": { code: "SC1002", docsUrl: "https://supacloud.dev/errors/SC1002" },
3385
+ "module-boundary-violation": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
3386
+ "module-boundary": { code: "SC1003", docsUrl: "https://supacloud.dev/errors/SC1003" },
3387
+ "circular-module-import": { code: "SC1004", docsUrl: "https://supacloud.dev/errors/SC1004" },
3388
+ "orphan-module": { code: "SC1005", docsUrl: "https://supacloud.dev/errors/SC1005" },
3389
+ "invalid-boundary-preset": { code: "SC1006", docsUrl: "https://supacloud.dev/errors/SC1006" },
3390
+ "circular-existing-alias": { code: "SC1007", docsUrl: "https://supacloud.dev/errors/SC1007" },
3391
+ "missing-deps": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
3392
+ "unresolved-token": { code: "SC2001", docsUrl: "https://supacloud.dev/errors/SC2001" },
3393
+ "duplicate-token": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
3394
+ "duplicate-module": { code: "SC2002", docsUrl: "https://supacloud.dev/errors/SC2002" },
3395
+ "disallow-controller-direct-db": { code: "SC2003", docsUrl: "https://supacloud.dev/errors/SC2003" },
3396
+ "self-dependency-violation": { code: "SC2004", docsUrl: "https://supacloud.dev/errors/SC2004" },
3397
+ "skip-self-dependency-violation": { code: "SC2005", docsUrl: "https://supacloud.dev/errors/SC2005" },
3398
+ "export-unprovided-token": { code: "SC2006", docsUrl: "https://supacloud.dev/errors/SC2006" },
3399
+ "unresolved-alias-target": { code: "SC2007", docsUrl: "https://supacloud.dev/errors/SC2007" },
3400
+ "self-referencing-alias": { code: "SC2008", docsUrl: "https://supacloud.dev/errors/SC2008" },
3401
+ "shadowed-route": { code: "SC3001", docsUrl: "https://supacloud.dev/errors/SC3001" },
3402
+ "unresolved-route-redirect": { code: "SC3002", docsUrl: "https://supacloud.dev/errors/SC3002" },
3403
+ "circular-route-redirect": { code: "SC3003", docsUrl: "https://supacloud.dev/errors/SC3003" },
3404
+ "invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
3405
+ "unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
3406
+ "missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
3407
+ "duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
3408
+ "missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
3409
+ "unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
3410
+ "malformed-route-path": { code: "SC3010", docsUrl: "https://supacloud.dev/errors/SC3010" },
3411
+ "duplicate-path-param": { code: "SC3011", docsUrl: "https://supacloud.dev/errors/SC3011" },
3412
+ "wildcard-not-trailing": { code: "SC3012", docsUrl: "https://supacloud.dev/errors/SC3012" },
3413
+ "invalid-query-param-name": { code: "SC3013", docsUrl: "https://supacloud.dev/errors/SC3013" },
3414
+ "unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
3415
+ "invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
3416
+ "disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
3417
+ "duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
3418
+ "conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
3419
+ "missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
3420
+ "missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
3421
+ "provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
3422
+ "unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
3423
+ "command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
3424
+ "duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
3425
+ "route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
3426
+ "command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
3427
+ "route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
3428
+ "command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
3429
+ "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3430
+ "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
3431
+ "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
3432
+ };
1193
3433
  function validateGraph(graph, options = false) {
1194
3434
  const strict = typeof options === "boolean" ? options : options.strict ?? false;
1195
3435
  const diagnostics = [];
@@ -1201,12 +3441,15 @@ function validateGraph(graph, options = false) {
1201
3441
  rules: options.moduleBoundaries
1202
3442
  });
1203
3443
  } catch (err) {
3444
+ const meta = COMPILER_DIAGNOSTIC_CODES["invalid-boundary-preset"];
1204
3445
  diagnostics.push({
1205
3446
  severity: "error",
1206
3447
  code: "invalid-boundary-preset",
1207
3448
  message: err instanceof Error ? err.message : String(err),
1208
3449
  file: graph.modules[0]?.file,
1209
- line: graph.modules[0]?.line
3450
+ line: graph.modules[0]?.line,
3451
+ errorCode: meta?.code,
3452
+ docsUrl: meta?.docsUrl
1210
3453
  });
1211
3454
  }
1212
3455
  }
@@ -1218,10 +3461,14 @@ function validateGraph(graph, options = false) {
1218
3461
  }
1219
3462
  }
1220
3463
  }
1221
- function resolveDep(module, token) {
1222
- const own = module.providers.find((p) => p.token === token);
1223
- if (own)
1224
- return { module, provider: own };
3464
+ function resolveDep(module, token, flags = {}) {
3465
+ if (!flags.skipSelf) {
3466
+ const own = module.providers.find((p) => p.token === token);
3467
+ if (own)
3468
+ return { module, provider: own };
3469
+ }
3470
+ if (flags.self)
3471
+ return;
1225
3472
  for (const importName of module.imports) {
1226
3473
  const imported = graph.modules.find((m) => m.name === importName);
1227
3474
  if (!imported || !imported.exports.includes(token))
@@ -1230,17 +3477,43 @@ function validateGraph(graph, options = false) {
1230
3477
  if (provider)
1231
3478
  return { module: imported, provider };
1232
3479
  }
3480
+ for (const mod of graph.modules) {
3481
+ const rootProvider = mod.providers.find((p) => p.token === token && p.providedIn === "root");
3482
+ if (rootProvider)
3483
+ return { module: mod, provider: rootProvider };
3484
+ }
1233
3485
  return;
1234
3486
  }
1235
- const error = (code, message, file, line) => {
1236
- diagnostics.push({ severity: "error", code, message, file, line });
3487
+ const error = (code, message, file, line, suggestion) => {
3488
+ const meta = COMPILER_DIAGNOSTIC_CODES[code];
3489
+ diagnostics.push({
3490
+ severity: "error",
3491
+ code,
3492
+ message,
3493
+ file,
3494
+ line,
3495
+ suggestion,
3496
+ errorCode: meta?.code,
3497
+ docsUrl: meta?.docsUrl
3498
+ });
1237
3499
  };
1238
- const warn2 = (code, message, file, line) => {
1239
- diagnostics.push({ severity: strict ? "error" : "warn", code, message, file, line });
3500
+ const warn2 = (code, message, file, line, suggestion) => {
3501
+ const meta = COMPILER_DIAGNOSTIC_CODES[code];
3502
+ diagnostics.push({
3503
+ severity: strict ? "error" : "warn",
3504
+ code,
3505
+ message,
3506
+ file,
3507
+ line,
3508
+ suggestion,
3509
+ errorCode: meta?.code,
3510
+ docsUrl: meta?.docsUrl
3511
+ });
1240
3512
  };
1241
3513
  const modulesByName = new Map;
1242
3514
  const commandsByName = new Map;
1243
3515
  const routesByKey = new Map;
3516
+ const declaredRoutes = [];
1244
3517
  for (const module of graph.modules) {
1245
3518
  const previousModule = modulesByName.get(module.name);
1246
3519
  if (previousModule) {
@@ -1260,20 +3533,128 @@ function validateGraph(graph, options = false) {
1260
3533
  for (const module of graph.modules) {
1261
3534
  for (const controller of module.controllers) {
1262
3535
  for (const route of controller.routes) {
1263
- const fullPath = joinRoutePaths(controller.path, route.path);
3536
+ if (route.path.includes("//") || controller.path.includes("//")) {
3537
+ 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.");
3538
+ } else if (/(^|\/):(\/|$)/.test(route.path) || route.path.endsWith("/:")) {
3539
+ 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').");
3540
+ } else if (route.path.includes("?") || route.path.includes("#")) {
3541
+ 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.");
3542
+ }
3543
+ if (route.path.includes("**")) {
3544
+ const segments = route.path.split("/").filter(Boolean);
3545
+ const wildcardIdx = segments.indexOf("**");
3546
+ if (wildcardIdx !== -1 && wildcardIdx !== segments.length - 1) {
3547
+ 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("/")}/**'.`);
3548
+ }
3549
+ }
3550
+ const fullPath = joinRoutePaths2(controller.path, route.path);
3551
+ const rawFullPath = joinRawRoutePaths(controller.path, route.path);
1264
3552
  const key = `${route.method} ${fullPath}`;
3553
+ const openApiMatch = route.path.match(/\{([a-zA-Z0-9_]+)\}/);
3554
+ if (openApiMatch) {
3555
+ 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]}'.`);
3556
+ }
1265
3557
  const previous = routesByKey.get(key);
1266
3558
  if (previous) {
1267
3559
  error("duplicate-route", `路由 ${key} 重复(首次声明于模块 ${previous.module.name} 的 ${previous.controller.className})`, controller.file);
1268
3560
  } else {
1269
3561
  routesByKey.set(key, { module, controller });
1270
3562
  }
3563
+ for (const prev of declaredRoutes) {
3564
+ if (prev.method === route.method && isRouteShadowed(prev.rawFullPath, rawFullPath)) {
3565
+ 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}'.`);
3566
+ }
3567
+ }
3568
+ declaredRoutes.push({ method: route.method, path: route.path, fullPath, rawFullPath, controller, module, handler: route.handler, redirectTo: route.redirectTo });
1271
3569
  if (route.command && !module.commands.some((command) => command.className === route.command)) {
1272
3570
  error("route-command-unresolved", `路由 ${key} 绑定的 command 类 ${route.command} 未在模块 ${module.name} 声明`, controller.file);
1273
3571
  }
3572
+ if ((route.method === "GET" || route.method === "HEAD") && route.command) {
3573
+ const boundCommand = module.commands.find((c) => c.className === route.command);
3574
+ if (boundCommand && boundCommand.transaction === "required") {
3575
+ 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'.`);
3576
+ }
3577
+ }
1274
3578
  if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
1275
3579
  error("route-command-binding-disallowed", `Route ${key} binds command ${route.command}, but route-level command bindings are disabled by policy. Use an application service (${controller.className}.${route.handler}, ${controller.file}).`, controller.file);
1276
3580
  }
3581
+ if (route.redirectTo) {
3582
+ const target = route.redirectTo.replace(/\/+$/, "");
3583
+ const current = fullPath.replace(/\/+$/, "");
3584
+ if (target === current || target === route.path.replace(/\/+$/, "")) {
3585
+ error("circular-route-redirect", `Route ${key} defines circular redirectTo '${route.redirectTo}'`, controller.file);
3586
+ }
3587
+ }
3588
+ const pathParams = route.pathParams ?? [];
3589
+ const seenParams = new Set;
3590
+ for (const p of pathParams) {
3591
+ if (seenParams.has(p)) {
3592
+ 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').`);
3593
+ }
3594
+ seenParams.add(p);
3595
+ }
3596
+ const paramBindings = route.paramBindings ?? [];
3597
+ for (const binding of paramBindings) {
3598
+ if (!binding || binding.trim().length === 0) {
3599
+ 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}'.`);
3600
+ } else if (/[#?&=/\s]/.test(binding)) {
3601
+ 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.`);
3602
+ } else if (!pathParams.includes(binding)) {
3603
+ const suggestion = findClosestMatch(binding, pathParams);
3604
+ 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);
3605
+ }
3606
+ }
3607
+ if (paramBindings.length > 0) {
3608
+ for (const param of pathParams) {
3609
+ if (!paramBindings.includes(param)) {
3610
+ 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.`);
3611
+ }
3612
+ }
3613
+ }
3614
+ const queryBindings = route.queryBindings ?? [];
3615
+ const seenQueries = new Set;
3616
+ for (const q of queryBindings) {
3617
+ if (!q || q.trim().length === 0) {
3618
+ 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').`);
3619
+ } else if (/[#?&=/\s]/.test(q)) {
3620
+ 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.`);
3621
+ } else if (seenQueries.has(q)) {
3622
+ 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}.`);
3623
+ }
3624
+ seenQueries.add(q);
3625
+ }
3626
+ if (route.queryDefaults && route.queryTransforms) {
3627
+ for (const [paramName, defVal] of Object.entries(route.queryDefaults)) {
3628
+ const transform = route.queryTransforms[paramName];
3629
+ if (transform === "number" && typeof defVal !== "number") {
3630
+ 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'.`);
3631
+ } else if (transform === "boolean" && typeof defVal !== "boolean") {
3632
+ 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.`);
3633
+ }
3634
+ }
3635
+ }
3636
+ if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
3637
+ 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().`);
3638
+ }
3639
+ if (route.hasBodyBinding && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
3640
+ 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().`);
3641
+ } else if (route.hasBodyBinding && !route.body) {
3642
+ 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.`);
3643
+ } else if (route.body && !route.hasBodyBinding && !route.command) {
3644
+ 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.`);
3645
+ }
3646
+ }
3647
+ const handlerMethodMap = new Map;
3648
+ for (const route of controller.routes) {
3649
+ const methods = handlerMethodMap.get(route.handler) ?? [];
3650
+ methods.push(route.method);
3651
+ handlerMethodMap.set(route.handler, methods);
3652
+ }
3653
+ for (const [handler, methods] of handlerMethodMap.entries()) {
3654
+ const uniqueMethods = Array.from(new Set(methods));
3655
+ if (uniqueMethods.length > 1) {
3656
+ 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.`);
3657
+ }
1277
3658
  }
1278
3659
  if (typeof options === "object" && options.disallowControllerDirectDb) {
1279
3660
  for (const dep of controller.deps) {
@@ -1283,6 +3664,73 @@ function validateGraph(graph, options = false) {
1283
3664
  }
1284
3665
  }
1285
3666
  }
3667
+ if (controller.selfDeps && controller.selfDeps.length > 0) {
3668
+ for (const dep of controller.selfDeps) {
3669
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
3670
+ if (!own) {
3671
+ error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
3672
+ }
3673
+ }
3674
+ }
3675
+ if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
3676
+ for (const dep of controller.skipSelfDeps) {
3677
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
3678
+ if (own) {
3679
+ 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().`);
3680
+ }
3681
+ }
3682
+ }
3683
+ }
3684
+ }
3685
+ const allTargetPaths = declaredRoutes.map((r) => r.rawFullPath);
3686
+ for (const item of declaredRoutes) {
3687
+ if (item.redirectTo) {
3688
+ const target = item.redirectTo;
3689
+ if (target.startsWith("/") && !target.startsWith("//")) {
3690
+ const normalizedTarget = target.replace(/\/+$/, "") || "/";
3691
+ const matchesTarget = declaredRoutes.some((candidate) => {
3692
+ if (candidate.rawFullPath === normalizedTarget)
3693
+ return true;
3694
+ return routeMatchesTarget(candidate.rawFullPath, normalizedTarget);
3695
+ });
3696
+ if (!matchesTarget) {
3697
+ const suggestion = findClosestMatch(normalizedTarget, allTargetPaths);
3698
+ 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);
3699
+ }
3700
+ }
3701
+ }
3702
+ }
3703
+ const routeByRawPath = new Map;
3704
+ for (const item of declaredRoutes) {
3705
+ if (!routeByRawPath.has(item.rawFullPath)) {
3706
+ routeByRawPath.set(item.rawFullPath, item);
3707
+ }
3708
+ }
3709
+ const reportedRedirectCycles = new Set;
3710
+ for (const item of declaredRoutes) {
3711
+ if (item.redirectTo) {
3712
+ const chain = [item.rawFullPath];
3713
+ let curr = item;
3714
+ while (curr && curr.redirectTo) {
3715
+ const target = curr.redirectTo.replace(/\/+$/, "") || "/";
3716
+ if (chain.includes(target)) {
3717
+ const cycle = [...chain.slice(chain.indexOf(target)), target];
3718
+ if (cycle.length > 2) {
3719
+ const cycleKey = [...cycle].sort().join("|");
3720
+ if (!reportedRedirectCycles.has(cycleKey)) {
3721
+ reportedRedirectCycles.add(cycleKey);
3722
+ const meta = COMPILER_DIAGNOSTIC_CODES["circular-route-redirect"];
3723
+ 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.");
3724
+ }
3725
+ }
3726
+ break;
3727
+ }
3728
+ chain.push(target);
3729
+ const next = routeByRawPath.get(target);
3730
+ if (!next || !next.redirectTo)
3731
+ break;
3732
+ curr = next;
3733
+ }
1286
3734
  }
1287
3735
  }
1288
3736
  for (const module of graph.modules) {
@@ -1290,33 +3738,63 @@ function validateGraph(graph, options = false) {
1290
3738
  for (const provider of module.providers) {
1291
3739
  const first = seen.get(provider.token);
1292
3740
  if (first) {
1293
- error("duplicate-token", `模块 ${module.name} 重复注册 token ${provider.token}(首次注册于 ${first.file}:${first.line})`, provider.file, provider.line);
3741
+ if (first.multi && provider.multi) {
3742
+ continue;
3743
+ }
3744
+ error("duplicate-token", `模块 ${module.name} 重复注册 token ${provider.token}(首次注册于 ${first.file}:${first.line})`, provider.file, provider.line, "If multiple providers are intended for this token, specify 'multi: true' on each provider definition (Angular multi-providers pattern).");
1294
3745
  } else {
1295
3746
  seen.set(provider.token, provider);
1296
3747
  }
1297
3748
  }
1298
3749
  for (const provider of module.providers) {
3750
+ if (provider.selfDeps && provider.selfDeps.length > 0) {
3751
+ for (const dep of provider.selfDeps) {
3752
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
3753
+ if (!own) {
3754
+ error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
3755
+ }
3756
+ }
3757
+ }
3758
+ if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
3759
+ for (const dep of provider.skipSelfDeps) {
3760
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
3761
+ if (own) {
3762
+ 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().`);
3763
+ }
3764
+ }
3765
+ }
1299
3766
  for (const dep of provider.deps) {
1300
- const resolved = resolveDep(module, dep);
3767
+ const isOptional = provider.optionalDeps?.includes(dep);
3768
+ const resolved = resolveDep(module, dep, {
3769
+ self: provider.selfDeps?.includes(dep),
3770
+ skipSelf: provider.skipSelfDeps?.includes(dep)
3771
+ });
1301
3772
  if (!resolved) {
3773
+ if (isOptional) {
3774
+ continue;
3775
+ }
1302
3776
  if (!graph.externalTokens.includes(dep)) {
1303
3777
  if (globalProviders.has(dep)) {
1304
3778
  const owner = globalProviders.get(dep);
1305
- error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line);
3779
+ if (!owner)
3780
+ continue;
3781
+ 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' }).`);
3782
+ } else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
3783
+ error("missing-token-factory", `InjectionToken '${dep}' referenced by provider '${provider.token}' has no provider in module '${module.name}' and no default factory function.`, provider.file, provider.line, `Provide '${dep}' in @Module({ providers: [...] }) or declare it with new InjectionToken('${dep}', { factory: () => ... }).`);
1306
3784
  } else {
1307
- error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line);
3785
+ error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line, `Provide '${dep}' in a module, mark constructor parameter @Optional(), or define @Injectable({ providedIn: 'root' }).`);
1308
3786
  }
1309
3787
  }
1310
3788
  continue;
1311
3789
  }
1312
3790
  if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
1313
- error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line);
3791
+ error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line, `Change provider '${provider.token}' scope to '${resolved.provider.scope}', or inject a factory/context instead.`);
1314
3792
  }
1315
3793
  }
1316
3794
  }
1317
3795
  for (const command of module.commands) {
1318
3796
  if (!command.permission) {
1319
- error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line);
3797
+ error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line, "Add 'permission: string' to @Command({ ... }) or configure command execution capabilities permission=false.");
1320
3798
  }
1321
3799
  if (typeof options === "object" && options.commandCapabilities) {
1322
3800
  const caps = options.commandCapabilities;
@@ -1360,7 +3838,8 @@ function validateGraph(graph, options = false) {
1360
3838
  }
1361
3839
  }
1362
3840
  if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
1363
- const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
3841
+ const allowedTags = rule.onlyDependOnLibsWithTags;
3842
+ const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
1364
3843
  if (!hasAllowed && targetTags.length > 0) {
1365
3844
  error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
1366
3845
  }
@@ -1369,18 +3848,120 @@ function validateGraph(graph, options = false) {
1369
3848
  }
1370
3849
  }
1371
3850
  }
3851
+ const referencedTokens = new Set;
3852
+ for (const mod of graph.modules) {
3853
+ for (const exp of mod.exports)
3854
+ referencedTokens.add(exp);
3855
+ for (const ctrl of mod.controllers) {
3856
+ for (const d of ctrl.deps ?? [])
3857
+ referencedTokens.add(d);
3858
+ for (const d of ctrl.optionalDeps ?? [])
3859
+ referencedTokens.add(d);
3860
+ for (const d of ctrl.selfDeps ?? [])
3861
+ referencedTokens.add(d);
3862
+ for (const d of ctrl.skipSelfDeps ?? [])
3863
+ referencedTokens.add(d);
3864
+ for (const d of ctrl.hostDeps ?? [])
3865
+ referencedTokens.add(d);
3866
+ }
3867
+ for (const p of mod.providers) {
3868
+ for (const d of p.deps ?? [])
3869
+ referencedTokens.add(d);
3870
+ for (const d of p.optionalDeps ?? [])
3871
+ referencedTokens.add(d);
3872
+ for (const d of p.selfDeps ?? [])
3873
+ referencedTokens.add(d);
3874
+ for (const d of p.skipSelfDeps ?? [])
3875
+ referencedTokens.add(d);
3876
+ for (const d of p.hostDeps ?? [])
3877
+ referencedTokens.add(d);
3878
+ if (p.useExisting)
3879
+ referencedTokens.add(p.useExisting);
3880
+ }
3881
+ }
3882
+ for (const mod of graph.modules) {
3883
+ for (const provider of mod.providers) {
3884
+ if (provider.providedIn === "root" && !provider.multi && !referencedTokens.has(provider.token) && !provider.exported) {
3885
+ 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.`);
3886
+ }
3887
+ }
3888
+ }
3889
+ for (const module of graph.modules) {
3890
+ for (const expToken of module.exports) {
3891
+ const resolved = resolveDep(module, expToken);
3892
+ if (resolved)
3893
+ continue;
3894
+ if (module.imports.includes(expToken))
3895
+ continue;
3896
+ 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.`);
3897
+ }
3898
+ }
3899
+ for (const module of graph.modules) {
3900
+ for (const provider of module.providers) {
3901
+ if (provider.useExisting) {
3902
+ const target = provider.useExisting;
3903
+ if (target === provider.token) {
3904
+ 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.`);
3905
+ } else {
3906
+ const resolved = resolveDep(module, target);
3907
+ if (!resolved && !graph.externalTokens.includes(target)) {
3908
+ 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.`);
3909
+ }
3910
+ }
3911
+ }
3912
+ }
3913
+ }
1372
3914
  diagnostics.push(...detectCycles(graph, resolveDep));
3915
+ diagnostics.push(...detectExistingAliasCycles(graph, resolveDep));
1373
3916
  diagnostics.push(...detectModuleCycles(graph));
1374
3917
  if (typeof options === "object" && options.detectOrphanModules) {
1375
3918
  diagnostics.push(...detectOrphanModules(graph));
1376
3919
  }
1377
3920
  return diagnostics;
1378
3921
  }
1379
- function joinRoutePaths(prefix, path) {
3922
+ function joinRoutePaths2(prefix, path) {
1380
3923
  const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
1381
3924
  const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
1382
3925
  return normalized.replace(/:[^/]+/g, ":param");
1383
3926
  }
3927
+ function joinRawRoutePaths(prefix, path) {
3928
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
3929
+ return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
3930
+ }
3931
+ function isRouteShadowed(earlierPath, laterPath) {
3932
+ const earlierSegments = earlierPath.split("/").filter(Boolean);
3933
+ const laterSegments = laterPath.split("/").filter(Boolean);
3934
+ if (earlierSegments.length !== laterSegments.length) {
3935
+ return false;
3936
+ }
3937
+ let hasParamShadowing = false;
3938
+ for (let i = 0;i < earlierSegments.length; i += 1) {
3939
+ const e = earlierSegments[i];
3940
+ const l = laterSegments[i];
3941
+ if (e === l) {
3942
+ continue;
3943
+ }
3944
+ if (e.startsWith(":") && !l.startsWith(":")) {
3945
+ hasParamShadowing = true;
3946
+ continue;
3947
+ }
3948
+ return false;
3949
+ }
3950
+ return hasParamShadowing;
3951
+ }
3952
+ function routeMatchesTarget(routePattern, targetPath) {
3953
+ const pSegs = routePattern.split("/").filter(Boolean);
3954
+ const tSegs = targetPath.split("/").filter(Boolean);
3955
+ if (pSegs.length !== tSegs.length)
3956
+ return false;
3957
+ for (let i = 0;i < pSegs.length; i += 1) {
3958
+ if (pSegs[i].startsWith(":"))
3959
+ continue;
3960
+ if (pSegs[i] !== tSegs[i])
3961
+ return false;
3962
+ }
3963
+ return true;
3964
+ }
1384
3965
  function detectCycles(graph, resolveDep) {
1385
3966
  const diagnostics = [];
1386
3967
  const nodeId = (ref) => `${ref.module.name}:${ref.provider.token}`;
@@ -1399,12 +3980,16 @@ function detectCycles(graph, resolveDep) {
1399
3980
  const cycleKey = cycle.map((item) => nodeId(item)).sort().join("|");
1400
3981
  if (!reported.has(cycleKey)) {
1401
3982
  reported.add(cycleKey);
3983
+ const meta = COMPILER_DIAGNOSTIC_CODES["circular-dependency"];
1402
3984
  diagnostics.push({
1403
3985
  severity: "error",
1404
3986
  code: "circular-dependency",
1405
3987
  message: `provider 循环依赖: ${path}`,
1406
3988
  file: ref.provider.file,
1407
- line: ref.provider.line
3989
+ line: ref.provider.line,
3990
+ suggestion: "Break the cycle by extracting common dependencies into a separate service or injecting @Optional().",
3991
+ errorCode: meta?.code,
3992
+ docsUrl: meta?.docsUrl
1408
3993
  });
1409
3994
  }
1410
3995
  return;
@@ -1412,7 +3997,10 @@ function detectCycles(graph, resolveDep) {
1412
3997
  state.set(id, "visiting");
1413
3998
  stack.push(ref);
1414
3999
  for (const dep of ref.provider.deps) {
1415
- const resolved = resolveDep(ref.module, dep);
4000
+ const resolved = resolveDep(ref.module, dep, {
4001
+ self: ref.provider.selfDeps?.includes(dep),
4002
+ skipSelf: ref.provider.skipSelfDeps?.includes(dep)
4003
+ });
1416
4004
  if (resolved)
1417
4005
  visit(resolved);
1418
4006
  }
@@ -1423,6 +4011,47 @@ function detectCycles(graph, resolveDep) {
1423
4011
  visit(ref);
1424
4012
  return diagnostics;
1425
4013
  }
4014
+ function detectExistingAliasCycles(graph, resolveDep) {
4015
+ const diagnostics = [];
4016
+ const existingProviders = [];
4017
+ for (const module of graph.modules) {
4018
+ for (const provider of module.providers) {
4019
+ if (provider.useExisting) {
4020
+ existingProviders.push({ module, provider });
4021
+ }
4022
+ }
4023
+ }
4024
+ const reported = new Set;
4025
+ for (const start of existingProviders) {
4026
+ const visited = [start.provider.token];
4027
+ let current = start;
4028
+ while (current && current.provider.useExisting) {
4029
+ const targetToken = current.provider.useExisting;
4030
+ if (visited.includes(targetToken)) {
4031
+ const cycle = [...visited.slice(visited.indexOf(targetToken)), targetToken];
4032
+ const cycleKey = [...cycle].sort().join("|");
4033
+ if (!reported.has(cycleKey)) {
4034
+ reported.add(cycleKey);
4035
+ const meta = COMPILER_DIAGNOSTIC_CODES["circular-existing-alias"];
4036
+ diagnostics.push({
4037
+ severity: "error",
4038
+ code: "circular-existing-alias",
4039
+ message: `Provider alias cycle detected in useExisting: ${cycle.join(" -> ")}`,
4040
+ file: start.provider.file,
4041
+ line: start.provider.line,
4042
+ suggestion: "Break the alias cycle by pointing useExisting to a concrete provider instead of a circular alias.",
4043
+ errorCode: meta?.code,
4044
+ docsUrl: meta?.docsUrl
4045
+ });
4046
+ }
4047
+ break;
4048
+ }
4049
+ visited.push(targetToken);
4050
+ current = resolveDep(current.module, targetToken);
4051
+ }
4052
+ }
4053
+ return diagnostics;
4054
+ }
1426
4055
  function detectModuleCycles(graph) {
1427
4056
  const diagnostics = [];
1428
4057
  const moduleMap = new Map(graph.modules.map((m) => [m.name, m]));
@@ -1439,12 +4068,16 @@ function detectModuleCycles(graph) {
1439
4068
  if (!reported.has(cycleKey)) {
1440
4069
  reported.add(cycleKey);
1441
4070
  const mod2 = moduleMap.get(name);
4071
+ const meta = COMPILER_DIAGNOSTIC_CODES["circular-module-import"];
1442
4072
  diagnostics.push({
1443
4073
  severity: "error",
1444
4074
  code: "circular-module-import",
1445
4075
  message: `Module circular import detected: ${cycle.join(" -> ")}`,
1446
4076
  file: mod2?.file,
1447
- line: mod2?.line
4077
+ line: mod2?.line,
4078
+ suggestion: "Refactor module imports into a unidirectional acyclic graph.",
4079
+ errorCode: meta?.code,
4080
+ docsUrl: meta?.docsUrl
1448
4081
  });
1449
4082
  }
1450
4083
  return;
@@ -1480,6 +4113,8 @@ function detectOrphanModules(graph) {
1480
4113
  }
1481
4114
  while (queue.length > 0) {
1482
4115
  const current = queue.shift();
4116
+ if (!current)
4117
+ continue;
1483
4118
  const mod = moduleMap.get(current);
1484
4119
  if (!mod)
1485
4120
  continue;
@@ -1492,12 +4127,15 @@ function detectOrphanModules(graph) {
1492
4127
  }
1493
4128
  for (const mod of graph.modules) {
1494
4129
  if (!reachable.has(mod.name)) {
4130
+ const meta = COMPILER_DIAGNOSTIC_CODES["orphan-module"];
1495
4131
  diagnostics.push({
1496
4132
  severity: "warn",
1497
4133
  code: "orphan-module",
1498
4134
  message: `Module '${mod.name}' is declared but not reachable from any root module (${rootModules.map((r) => r.name).join(", ")})`,
1499
4135
  file: mod.file,
1500
- line: mod.line
4136
+ line: mod.line,
4137
+ errorCode: meta?.code,
4138
+ docsUrl: meta?.docsUrl
1501
4139
  });
1502
4140
  }
1503
4141
  }
@@ -1505,10 +4143,225 @@ function detectOrphanModules(graph) {
1505
4143
  }
1506
4144
 
1507
4145
  // src/compile.ts
1508
- import { existsSync as existsSync2, readFileSync } from "node:fs";
1509
- import { join as join3 } from "node:path";
4146
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4147
+ import { join as join4 } from "node:path";
4148
+
4149
+ // src/type-safety.ts
4150
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4151
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4152
+ import * as ts4 from "@typescript/typescript6";
4153
+ var DEFAULT_EXCLUDES = [
4154
+ "**/*.test.ts",
4155
+ "**/*.spec.ts",
4156
+ "**/test/**",
4157
+ "**/tests/**",
4158
+ "**/__tests__/**",
4159
+ "**/fixtures/**",
4160
+ "**/generated/**",
4161
+ "**/dist/**",
4162
+ "**/*.d.ts"
4163
+ ];
4164
+ var DIAGNOSTIC_META = {
4165
+ "generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
4166
+ "source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
4167
+ "source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
4168
+ "source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
4169
+ "source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
4170
+ };
4171
+ function scanGeneratedArtifacts(artifacts, strict = true) {
4172
+ const diagnostics = [];
4173
+ for (const [file, content] of Object.entries(artifacts)) {
4174
+ if (content === undefined)
4175
+ continue;
4176
+ const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
4177
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4178
+ diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
4179
+ }
4180
+ }
4181
+ return diagnostics;
4182
+ }
4183
+ function scanProductionSource(options) {
4184
+ const rootDir = resolve2(options.rootDir);
4185
+ const configPath = join3(rootDir, "tsconfig.json");
4186
+ const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
4187
+ options: {
4188
+ strict: true,
4189
+ skipLibCheck: true,
4190
+ target: ts4.ScriptTarget.ES2022,
4191
+ module: ts4.ModuleKind.ESNext
4192
+ },
4193
+ errors: []
4194
+ };
4195
+ const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
4196
+ const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
4197
+ const compilerOptions = { ...projectConfig.options, noEmit: true };
4198
+ const host = ts4.createCompilerHost(compilerOptions);
4199
+ host.getCurrentDirectory = () => rootDir;
4200
+ const program = ts4.createProgram(rootNames, compilerOptions, host);
4201
+ const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
4202
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
4203
+ const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
4204
+ const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
4205
+ severity: "error",
4206
+ code: "source-config",
4207
+ message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
4208
+ `),
4209
+ file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
4210
+ line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
4211
+ errorCode: `TS${diagnostic.code}`
4212
+ }));
4213
+ const checker = program.getTypeChecker();
4214
+ for (const sourceFile of sourceFiles) {
4215
+ scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
4216
+ }
4217
+ return diagnostics;
4218
+ }
4219
+ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
4220
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4221
+ diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
4222
+ }
4223
+ for (const node of descendants(sourceFile)) {
4224
+ if (ts4.isAsExpression(node)) {
4225
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4226
+ continue;
4227
+ const assertedType = node.type.getText(sourceFile);
4228
+ if (assertedType === "const")
4229
+ continue;
4230
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4231
+ } else if (ts4.isTypeAssertionExpression(node)) {
4232
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4233
+ continue;
4234
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4235
+ } else if (ts4.isNonNullExpression(node)) {
4236
+ diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
4237
+ }
4238
+ }
4239
+ for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
4240
+ const initializer = declaration.initializer;
4241
+ if (!initializer || declaration.type)
4242
+ continue;
4243
+ const declarationType = checker.getTypeAtLocation(declaration.name);
4244
+ const initializerType = checker.getTypeAtLocation(initializer);
4245
+ for (const name of bindingNames(declaration.name)) {
4246
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4247
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
4248
+ }
4249
+ }
4250
+ if (isAnyType(declarationType))
4251
+ continue;
4252
+ if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
4253
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
4254
+ }
4255
+ if (ts4.isObjectLiteralExpression(initializer) && isConstDeclaration(declaration) && initializer.getText(sourceFile).length > 0 && initializer.properties.some((property) => ts4.isPropertyAssignment(property) && property.initializer !== undefined && !ts4.isAsExpression(property.initializer) && isLiteralExpression(property.initializer))) {
4256
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
4257
+ }
4258
+ }
4259
+ for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
4260
+ if (parameter.type)
4261
+ continue;
4262
+ for (const name of bindingNames(parameter.name)) {
4263
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4264
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
4265
+ }
4266
+ }
4267
+ }
4268
+ }
4269
+ function readProjectConfig2(configPath) {
4270
+ const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
4271
+ if (config.error)
4272
+ return { options: {}, errors: [config.error] };
4273
+ const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
4274
+ return { options: parsed.options, errors: parsed.errors };
4275
+ }
4276
+ function isProductionSource(rootDir, sourceFile, excludes, outDir) {
4277
+ const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
4278
+ if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
4279
+ return false;
4280
+ if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
4281
+ return false;
4282
+ return !excludes.some((pattern) => globMatches(relativePath, pattern));
4283
+ }
4284
+ function isProductionSourcePath(rootDir, filePath, excludes) {
4285
+ const relativePath = normalizeRelative(rootDir, filePath);
4286
+ return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
4287
+ }
4288
+ function globMatches(value, pattern) {
4289
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
4290
+ return new RegExp(`^${escaped}$`).test(value);
4291
+ }
4292
+ function bindingNames(name) {
4293
+ if (ts4.isIdentifier(name))
4294
+ return [name];
4295
+ return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
4296
+ }
4297
+ function isLiteralExpression(node) {
4298
+ if (!node)
4299
+ return false;
4300
+ return [
4301
+ ts4.SyntaxKind.StringLiteral,
4302
+ ts4.SyntaxKind.NumericLiteral,
4303
+ ts4.SyntaxKind.TrueKeyword,
4304
+ ts4.SyntaxKind.FalseKeyword
4305
+ ].includes(node.kind);
4306
+ }
4307
+ function isLiteralSyntax(node) {
4308
+ return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
4309
+ }
4310
+ function isLiteralType(type) {
4311
+ return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
4312
+ }
4313
+ function isAnyType(type) {
4314
+ return (type.flags & ts4.TypeFlags.Any) !== 0;
4315
+ }
4316
+ function isLetDeclaration(declaration) {
4317
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4318
+ }
4319
+ function isConstDeclaration(declaration) {
4320
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
4321
+ }
4322
+ function descendants(root) {
4323
+ const result = [];
4324
+ const visit = (node) => {
4325
+ result.push(node);
4326
+ ts4.forEachChild(node, visit);
4327
+ };
4328
+ ts4.forEachChild(root, visit);
4329
+ return result;
4330
+ }
4331
+ function descendantsOfKind2(root, predicate) {
4332
+ const result = [];
4333
+ const visit = (node) => {
4334
+ if (predicate(node))
4335
+ result.push(node);
4336
+ ts4.forEachChild(node, visit);
4337
+ };
4338
+ ts4.forEachChild(root, visit);
4339
+ return result;
4340
+ }
4341
+ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
4342
+ const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
4343
+ const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
4344
+ const meta = DIAGNOSTIC_META[code];
4345
+ return {
4346
+ severity: strict ? "error" : "warn",
4347
+ code,
4348
+ message,
4349
+ file,
4350
+ line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
4351
+ errorCode: meta.errorCode,
4352
+ docsUrl: meta.docsUrl
4353
+ };
4354
+ }
4355
+ function normalizeRelative(rootDir, filePath) {
4356
+ return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4357
+ }
4358
+ function isAnyKeyword(node) {
4359
+ return node.kind === ts4.SyntaxKind.AnyKeyword;
4360
+ }
4361
+
4362
+ // src/compile.ts
1510
4363
  async function compileProject(options) {
1511
- const graph = await analyzeProject(options.rootDir, options.include);
4364
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
1512
4365
  const diagnostics = [
1513
4366
  ...graph.diagnostics ?? [],
1514
4367
  ...validateGraph(graph, {
@@ -1527,14 +4380,51 @@ async function compileProject(options) {
1527
4380
  diagnostic.severity = "error";
1528
4381
  }
1529
4382
  }
1530
- const written = await generateApplication(graph, {
4383
+ const typeSafety = resolveTypeSafety(options);
4384
+ const rendered = renderApplication(graph, {
1531
4385
  rootDir: options.rootDir,
1532
- outDir: options.outDir
4386
+ outDir: options.outDir,
4387
+ generateClient: options.generateClient,
4388
+ generatePermissions: options.generatePermissions,
4389
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
1533
4390
  });
1534
- return { diagnostics, graph, written };
4391
+ if (typeSafety.scanProductionSource) {
4392
+ diagnostics.push(...scanProductionSource({
4393
+ rootDir: options.rootDir,
4394
+ include: options.include,
4395
+ outDir: options.outDir,
4396
+ strict: options.strict,
4397
+ ...typeSafety
4398
+ }));
4399
+ }
4400
+ if (typeSafety.noAnyInGenerated) {
4401
+ diagnostics.push(...scanGeneratedArtifacts({
4402
+ "application.ts": rendered.applicationCode,
4403
+ "client.ts": rendered.clientCode,
4404
+ "permissions.ts": rendered.permissionsCode
4405
+ }, options.strict ?? false));
4406
+ }
4407
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
4408
+ const generatedOptions = {
4409
+ rootDir: options.rootDir,
4410
+ outDir: options.outDir,
4411
+ generateClient: options.generateClient,
4412
+ generatePermissions: options.generatePermissions,
4413
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4414
+ artifactHashes: options.cache?.generatedHashes
4415
+ };
4416
+ const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
4417
+ const stats = graph.cacheStats ? {
4418
+ cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
4419
+ changedFiles: [],
4420
+ affectedModules: graph.cacheStats.reanalyzedModules,
4421
+ reanalyzedModules: graph.cacheStats.reanalyzedModules,
4422
+ reusedModules: graph.cacheStats.reusedModules
4423
+ } : undefined;
4424
+ return { diagnostics, graph, written, stats };
1535
4425
  }
1536
4426
  async function checkProject(options) {
1537
- const graph = await analyzeProject(options.rootDir, options.include);
4427
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
1538
4428
  const diagnostics = [
1539
4429
  ...graph.diagnostics ?? [],
1540
4430
  ...validateGraph(graph, {
@@ -1553,22 +4443,48 @@ async function checkProject(options) {
1553
4443
  diagnostic.severity = "error";
1554
4444
  }
1555
4445
  }
4446
+ const typeSafety = resolveTypeSafety(options);
1556
4447
  const rendered = renderApplication(graph, {
1557
4448
  rootDir: options.rootDir,
1558
- outDir: options.outDir
4449
+ outDir: options.outDir,
4450
+ generateClient: options.generateClient,
4451
+ generatePermissions: options.generatePermissions,
4452
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
1559
4453
  });
4454
+ if (typeSafety.scanProductionSource) {
4455
+ diagnostics.push(...scanProductionSource({
4456
+ rootDir: options.rootDir,
4457
+ include: options.include,
4458
+ outDir: options.outDir,
4459
+ strict: options.strict,
4460
+ ...typeSafety
4461
+ }));
4462
+ }
4463
+ if (typeSafety.noAnyInGenerated) {
4464
+ diagnostics.push(...scanGeneratedArtifacts({
4465
+ "application.ts": rendered.applicationCode,
4466
+ "client.ts": rendered.clientCode,
4467
+ "permissions.ts": rendered.permissionsCode
4468
+ }, options.strict ?? false));
4469
+ }
1560
4470
  const expectedFiles = {
1561
4471
  "application.ts": rendered.applicationCode,
1562
4472
  "app.manifest.json": rendered.manifestJson
1563
4473
  };
4474
+ if (rendered.clientCode) {
4475
+ expectedFiles["client.ts"] = rendered.clientCode;
4476
+ }
4477
+ if (rendered.permissionsCode) {
4478
+ expectedFiles["permissions.ts"] = rendered.permissionsCode;
4479
+ }
1564
4480
  const mismatches = [];
1565
4481
  for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
1566
- const diskPath = join3(options.outDir, filename);
1567
- if (!existsSync2(diskPath)) {
4482
+ const diskPath = join4(options.outDir, filename);
4483
+ if (!existsSync3(diskPath)) {
1568
4484
  mismatches.push(`${filename}: generated artifact is missing from disk`);
1569
4485
  continue;
1570
4486
  }
1571
- const diskContent = readFileSync(diskPath, "utf8");
4487
+ const diskContent = readFileSync3(diskPath, "utf8");
1572
4488
  if (diskContent !== expectedContent) {
1573
4489
  mismatches.push(`${filename}: disk artifact differs from current compiler output`);
1574
4490
  }
@@ -1580,19 +4496,547 @@ async function checkProject(options) {
1580
4496
  graph
1581
4497
  };
1582
4498
  }
4499
+ function resolveTypeSafety(options) {
4500
+ return {
4501
+ noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
4502
+ scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
4503
+ exclude: options.typeSafety?.exclude
4504
+ };
4505
+ }
4506
+ // src/watch.ts
4507
+ import { watch } from "node:fs";
4508
+ import { relative as relative4, resolve as resolve4 } from "node:path";
4509
+
4510
+ // src/incremental.ts
4511
+ import { createHash as createHash5 } from "node:crypto";
4512
+ import { access as access2, readdir, readFile } from "node:fs/promises";
4513
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
4514
+ function createDependencyGraphCache() {
4515
+ return {
4516
+ modules: new Map,
4517
+ fileHashes: new Map,
4518
+ generatedHashes: new Map
4519
+ };
4520
+ }
4521
+ function createIncrementalCompiler() {
4522
+ let previousSnapshot;
4523
+ let previousResult;
4524
+ let previousCache;
4525
+ const cache = createDependencyGraphCache();
4526
+ return {
4527
+ async compile(options, changedPaths) {
4528
+ const optionsKey = optionsKeyOf(options);
4529
+ const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
4530
+ const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
4531
+ const activeCache = options.cache ?? cache;
4532
+ const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
4533
+ if (cacheHit && previousResult) {
4534
+ return {
4535
+ ...previousResult,
4536
+ stats: {
4537
+ cacheHit: true,
4538
+ changedFiles: [],
4539
+ affectedModules: [],
4540
+ reusedModules: previousResult.graph.modules.map((m) => m.name),
4541
+ reanalyzedModules: []
4542
+ }
4543
+ };
4544
+ }
4545
+ if (!activeCache.dependencyGraph && previousResult) {
4546
+ activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
4547
+ }
4548
+ const result = await compileProject({
4549
+ ...options,
4550
+ cache: activeCache,
4551
+ changedPaths: changedFiles
4552
+ });
4553
+ const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
4554
+ const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
4555
+ const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
4556
+ if (activeCache) {
4557
+ activeCache.dependencyGraph = new ModuleDependencyGraph(result.graph.modules);
4558
+ }
4559
+ const stats = {
4560
+ cacheHit: false,
4561
+ changedFiles,
4562
+ affectedModules,
4563
+ reusedModules,
4564
+ reanalyzedModules
4565
+ };
4566
+ previousSnapshot = snapshot;
4567
+ previousResult = result;
4568
+ previousCache = activeCache;
4569
+ return { ...result, stats };
4570
+ },
4571
+ reset() {
4572
+ previousSnapshot = undefined;
4573
+ previousResult = undefined;
4574
+ previousCache = undefined;
4575
+ cache.modules.clear();
4576
+ cache.fileHashes.clear();
4577
+ cache.generatedHashes?.clear();
4578
+ cache.dependencyGraph = undefined;
4579
+ cache.programSession?.reset();
4580
+ },
4581
+ getCache() {
4582
+ return cache;
4583
+ }
4584
+ };
4585
+ }
4586
+ async function updateSnapshot(previous, options, changedPaths) {
4587
+ const rootDir = resolve3(options.rootDir);
4588
+ const outDir = resolve3(options.outDir);
4589
+ const files = { ...previous.files };
4590
+ for (const changedPath of changedPaths) {
4591
+ const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4592
+ const relativeChangedPath = relative3(rootDir, absolutePath);
4593
+ if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
4594
+ continue;
4595
+ if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
4596
+ continue;
4597
+ const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
4598
+ try {
4599
+ await access2(absolutePath);
4600
+ const content = await readFile(absolutePath);
4601
+ files[relativePath] = createHash5("sha256").update(content).digest("hex");
4602
+ } catch {
4603
+ delete files[relativePath];
4604
+ }
4605
+ }
4606
+ return { files, optionsKey: optionsKeyOf(options) };
4607
+ }
4608
+ async function createSnapshot(options) {
4609
+ const rootDir = resolve3(options.rootDir);
4610
+ const outDir = resolve3(options.outDir);
4611
+ const paths = await listSourceFiles(rootDir, outDir);
4612
+ const files = {};
4613
+ for (const path of paths) {
4614
+ const content = await readFile(path);
4615
+ files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
4616
+ }
4617
+ return { files, optionsKey: optionsKeyOf(options) };
4618
+ }
4619
+ function optionsKeyOf(options) {
4620
+ return JSON.stringify({
4621
+ rootDir: resolve3(options.rootDir),
4622
+ outDir: resolve3(options.outDir),
4623
+ include: options.include,
4624
+ strict: options.strict,
4625
+ writeOnError: options.writeOnError,
4626
+ moduleBoundaryPreset: options.moduleBoundaryPreset,
4627
+ moduleBoundaries: options.moduleBoundaries,
4628
+ allowRouteCommandBindings: options.allowRouteCommandBindings,
4629
+ commandCapabilities: options.commandCapabilities,
4630
+ disallowControllerDirectDb: options.disallowControllerDirectDb,
4631
+ detectOrphanModules: options.detectOrphanModules,
4632
+ generateClient: options.generateClient,
4633
+ generatePermissions: options.generatePermissions,
4634
+ typeSafety: options.typeSafety,
4635
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
4636
+ });
4637
+ }
4638
+ async function listSourceFiles(rootDir, outDir) {
4639
+ const result = [];
4640
+ const visit = async (directory) => {
4641
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
4642
+ const path = resolve3(directory, entry.name);
4643
+ if (entry.isDirectory()) {
4644
+ if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
4645
+ continue;
4646
+ await visit(path);
4647
+ } else if (/\.(tsx?|mts|cts)$/.test(entry.name)) {
4648
+ result.push(path);
4649
+ }
4650
+ }
4651
+ };
4652
+ await visit(rootDir);
4653
+ return result.sort();
4654
+ }
4655
+ function diffFiles(previous, current) {
4656
+ if (!previous)
4657
+ return Object.keys(current);
4658
+ const names = new Set([...Object.keys(previous), ...Object.keys(current)]);
4659
+ return [...names].filter((name) => previous[name] !== current[name]).sort();
4660
+ }
4661
+
4662
+ class ModuleDependencyGraph {
4663
+ imports = new Map;
4664
+ dependents = new Map;
4665
+ fileOwners = new Map;
4666
+ moduleMap = new Map;
4667
+ constructor(modules = []) {
4668
+ this.rebuild(modules);
4669
+ }
4670
+ rebuild(modules) {
4671
+ this.imports.clear();
4672
+ this.dependents.clear();
4673
+ this.fileOwners.clear();
4674
+ this.moduleMap.clear();
4675
+ for (const mod of modules) {
4676
+ this.moduleMap.set(mod.name, mod);
4677
+ this.imports.set(mod.name, new Set(mod.imports));
4678
+ if (!this.dependents.has(mod.name)) {
4679
+ this.dependents.set(mod.name, new Set);
4680
+ }
4681
+ this.indexFile(mod.file, mod.name);
4682
+ for (const p of mod.providers) {
4683
+ if (p.importPath)
4684
+ this.indexFile(p.importPath, mod.name);
4685
+ if (p.file)
4686
+ this.indexFile(p.file, mod.name);
4687
+ }
4688
+ for (const c of mod.controllers) {
4689
+ if (c.importPath)
4690
+ this.indexFile(c.importPath, mod.name);
4691
+ if (c.file)
4692
+ this.indexFile(c.file, mod.name);
4693
+ }
4694
+ }
4695
+ for (const [modName, imps] of this.imports.entries()) {
4696
+ for (const imp of imps) {
4697
+ if (!this.dependents.has(imp)) {
4698
+ this.dependents.set(imp, new Set);
4699
+ }
4700
+ const dependents = this.dependents.get(imp);
4701
+ if (dependents)
4702
+ dependents.add(modName);
4703
+ }
4704
+ }
4705
+ }
4706
+ indexFile(path, moduleName) {
4707
+ if (!path)
4708
+ return;
4709
+ const normalized = path.replace(/\.(tsx?|mts|cts)$/, "");
4710
+ if (!this.fileOwners.has(normalized)) {
4711
+ this.fileOwners.set(normalized, new Set);
4712
+ }
4713
+ const owners = this.fileOwners.get(normalized);
4714
+ if (owners)
4715
+ owners.add(moduleName);
4716
+ }
4717
+ getModulesOwningFile(filePath) {
4718
+ const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
4719
+ return Array.from(this.fileOwners.get(normalized) ?? []);
4720
+ }
4721
+ getAffectedModules(changedFiles) {
4722
+ if (changedFiles.length === 0)
4723
+ return [];
4724
+ const directlyAffected = new Set;
4725
+ for (const file of changedFiles) {
4726
+ for (const modName of this.getModulesOwningFile(file)) {
4727
+ directlyAffected.add(modName);
4728
+ }
4729
+ }
4730
+ if (directlyAffected.size === 0) {
4731
+ return [];
4732
+ }
4733
+ const affected = new Set(directlyAffected);
4734
+ const queue = Array.from(directlyAffected);
4735
+ while (queue.length > 0) {
4736
+ const current = queue.shift();
4737
+ if (!current)
4738
+ continue;
4739
+ const dependents = this.dependents.get(current);
4740
+ if (dependents) {
4741
+ for (const dep of dependents) {
4742
+ if (!affected.has(dep)) {
4743
+ affected.add(dep);
4744
+ queue.push(dep);
4745
+ }
4746
+ }
4747
+ }
4748
+ }
4749
+ return Array.from(this.moduleMap.keys()).filter((name) => affected.has(name));
4750
+ }
4751
+ getDirectImports(moduleName) {
4752
+ return Array.from(this.imports.get(moduleName) ?? []);
4753
+ }
4754
+ getDirectDependents(moduleName) {
4755
+ return Array.from(this.dependents.get(moduleName) ?? []);
4756
+ }
4757
+ }
4758
+ function findAffectedModules(previous, current, changedFiles) {
4759
+ const depGraph = new ModuleDependencyGraph(current);
4760
+ return depGraph.getAffectedModules(changedFiles);
4761
+ }
4762
+
4763
+ // src/watch.ts
4764
+ var DEFAULT_DEBOUNCE_MS = 100;
4765
+ function watchProject(options) {
4766
+ const rootDir = resolve4(options.rootDir);
4767
+ const outDir = resolve4(options.outDir);
4768
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
4769
+ let timer;
4770
+ let closed = false;
4771
+ let compiling = false;
4772
+ let pending = false;
4773
+ const pendingPaths = new Set;
4774
+ let watcher;
4775
+ const incremental = createIncrementalCompiler();
4776
+ let initialEvent;
4777
+ let resolveReady = () => {
4778
+ return;
4779
+ };
4780
+ let rejectReady = () => {
4781
+ return;
4782
+ };
4783
+ const ready = new Promise((resolvePromise, rejectPromise) => {
4784
+ resolveReady = resolvePromise;
4785
+ rejectReady = rejectPromise;
4786
+ });
4787
+ const emit = (event) => {
4788
+ options.onEvent?.(event);
4789
+ if (event.initial)
4790
+ initialEvent = event;
4791
+ };
4792
+ const compile = async (initial, changedPaths = []) => {
4793
+ if (closed && !initial)
4794
+ return;
4795
+ if (compiling) {
4796
+ pending = true;
4797
+ return;
4798
+ }
4799
+ compiling = true;
4800
+ const startedAt = performance.now();
4801
+ options.onEvent?.({
4802
+ type: "compile-start",
4803
+ initial,
4804
+ durationMs: 0,
4805
+ diagnostics: [],
4806
+ written: []
4807
+ });
4808
+ try {
4809
+ const result = await incremental.compile({ ...options, writeOnError: false }, changedPaths);
4810
+ const durationMs = Math.round(performance.now() - startedAt);
4811
+ const hasErrors = result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
4812
+ emit({
4813
+ type: hasErrors ? "compile-error" : "compiled",
4814
+ initial,
4815
+ durationMs,
4816
+ diagnostics: result.diagnostics,
4817
+ written: result.written,
4818
+ stats: result.stats
4819
+ });
4820
+ } catch (error) {
4821
+ rejectReady(error);
4822
+ throw error;
4823
+ } finally {
4824
+ compiling = false;
4825
+ if (pending && !closed) {
4826
+ pending = false;
4827
+ compile(false, [...pendingPaths]);
4828
+ pendingPaths.clear();
4829
+ }
4830
+ }
4831
+ };
4832
+ const schedule = (changedPath) => {
4833
+ if (closed)
4834
+ return;
4835
+ if (changedPath)
4836
+ pendingPaths.add(changedPath);
4837
+ if (timer)
4838
+ clearTimeout(timer);
4839
+ timer = setTimeout(() => {
4840
+ timer = undefined;
4841
+ compile(false, [...pendingPaths]);
4842
+ pendingPaths.clear();
4843
+ }, debounceMs);
4844
+ };
4845
+ compile(true).then(() => {
4846
+ if (closed)
4847
+ return;
4848
+ watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
4849
+ if (!filename)
4850
+ return schedule();
4851
+ const changedPath = resolve4(rootDir, filename.toString());
4852
+ const relativePath = relative4(outDir, changedPath);
4853
+ if (!relativePath.startsWith("..") && relativePath !== "")
4854
+ return;
4855
+ if (/\.(tsx?|mts|cts)$/.test(changedPath))
4856
+ schedule(relative4(rootDir, changedPath));
4857
+ });
4858
+ if (initialEvent)
4859
+ resolveReady(initialEvent);
4860
+ }).catch(() => {
4861
+ return;
4862
+ });
4863
+ return {
4864
+ ready,
4865
+ async close() {
4866
+ closed = true;
4867
+ if (timer)
4868
+ clearTimeout(timer);
4869
+ watcher?.close();
4870
+ await ready.catch(() => {
4871
+ return;
4872
+ });
4873
+ }
4874
+ };
4875
+ }
4876
+ // src/inspect.ts
4877
+ import { existsSync as existsSync4 } from "node:fs";
4878
+ import { join as join5 } from "node:path";
4879
+ function formatGraph(graph) {
4880
+ const lines = [];
4881
+ for (const module of graph.modules) {
4882
+ lines.push(`MODULE ${module.name}`);
4883
+ lines.push(` file: ${module.file}:${module.line}`);
4884
+ lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
4885
+ lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
4886
+ lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
4887
+ lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
4888
+ }
4889
+ lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
4890
+ return lines.join(`
4891
+ `);
4892
+ }
4893
+ function explainGraph(graph, subject) {
4894
+ const module = graph.modules.find((candidate) => candidate.name === subject);
4895
+ if (module)
4896
+ return explainModule(graph, module);
4897
+ const provider = findProvider(graph, subject);
4898
+ if (provider)
4899
+ return explainProvider(graph, provider.module, provider.provider);
4900
+ if (graph.externalTokens.includes(subject)) {
4901
+ const references = graph.modules.flatMap((candidate) => [
4902
+ ...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
4903
+ ...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
4904
+ ]);
4905
+ return [
4906
+ `EXTERNAL TOKEN ${subject}`,
4907
+ " provided by: platform runtime",
4908
+ ` references: ${references.length > 0 ? references.join(", ") : "-"}`
4909
+ ].join(`
4910
+ `);
4911
+ }
4912
+ const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
4913
+ throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
4914
+ }
4915
+ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
4916
+ const checks = [
4917
+ {
4918
+ name: "project-root",
4919
+ ok: existsSync4(rootDir),
4920
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
4921
+ },
4922
+ {
4923
+ name: "tsconfig",
4924
+ ok: existsSync4(join5(rootDir, "tsconfig.json")),
4925
+ detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
4926
+ },
4927
+ {
4928
+ name: "modules",
4929
+ ok: graph.modules.length > 0,
4930
+ detail: `${graph.modules.length} module(s) discovered`
4931
+ },
4932
+ {
4933
+ name: "generated-artifacts",
4934
+ ok: upToDate,
4935
+ detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
4936
+ }
4937
+ ];
4938
+ const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
4939
+ return {
4940
+ checks,
4941
+ diagnostics: allDiagnostics,
4942
+ errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
4943
+ };
4944
+ }
4945
+ function explainModule(graph, module) {
4946
+ const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
4947
+ return [
4948
+ `MODULE ${module.name}`,
4949
+ ` file: ${module.file}:${module.line}`,
4950
+ ` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
4951
+ ` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
4952
+ ` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
4953
+ ` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
4954
+ ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`
4955
+ ].join(`
4956
+ `);
4957
+ }
4958
+ function explainProvider(graph, module, provider) {
4959
+ const dependents = graph.modules.flatMap((candidate) => [
4960
+ ...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
4961
+ ...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
4962
+ ]);
4963
+ return [
4964
+ `PROVIDER ${provider.token}`,
4965
+ ` module: ${module.name}`,
4966
+ ` file: ${provider.file}:${provider.line}`,
4967
+ ` kind: ${provider.kind}`,
4968
+ ` scope: ${provider.scope}`,
4969
+ ` exported: ${provider.exported ? "yes" : "no"}`,
4970
+ ` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
4971
+ ` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
4972
+ ].join(`
4973
+ `);
4974
+ }
4975
+ function findProvider(graph, subject) {
4976
+ for (const module of graph.modules) {
4977
+ const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
4978
+ if (provider)
4979
+ return { module, provider };
4980
+ }
4981
+ return;
4982
+ }
4983
+ function exportGraphMermaid(graph) {
4984
+ const lines = ["graph TD"];
4985
+ for (const mod of graph.modules) {
4986
+ const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
4987
+ lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
4988
+ for (const imp of mod.imports) {
4989
+ const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
4990
+ lines.push(` ${safeId} --> ${safeImp}`);
4991
+ }
4992
+ }
4993
+ return lines.join(`
4994
+ `);
4995
+ }
4996
+ function exportGraphDot(graph) {
4997
+ const lines = [
4998
+ "digraph ApplicationGraph {",
4999
+ " rankdir=LR;",
5000
+ ' node [shape=box, fontname="Helvetica"];'
5001
+ ];
5002
+ for (const mod of graph.modules) {
5003
+ lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
5004
+ for (const imp of mod.imports) {
5005
+ lines.push(` "${mod.name}" -> "${imp}";`);
5006
+ }
5007
+ }
5008
+ lines.push("}");
5009
+ return lines.join(`
5010
+ `);
5011
+ }
1583
5012
  export {
1584
5013
  ANGULAR_ENTERPRISE_RULES,
1585
5014
  CLEAN_ARCHITECTURE_RULES,
5015
+ COMPILER_DIAGNOSTIC_CODES,
1586
5016
  MODULAR_MONOLITH_RULES,
1587
5017
  MODULE_BOUNDARY_PROFILES,
5018
+ ModuleDependencyGraph,
5019
+ TraitCompiler,
1588
5020
  analyzeProject,
1589
5021
  camelName,
1590
5022
  checkProject,
1591
5023
  compileProject,
5024
+ compileTraits,
5025
+ createDependencyGraphCache,
5026
+ createIncrementalCompiler,
5027
+ createIncrementalProgramSession,
5028
+ doctorProject,
5029
+ explainGraph,
5030
+ exportGraphDot,
5031
+ exportGraphMermaid,
5032
+ formatGraph,
1592
5033
  generateApplication,
1593
5034
  getModuleBoundaryPreset,
1594
5035
  getModuleBoundaryProfile,
1595
5036
  renderApplication,
1596
5037
  resolveModuleBoundaries,
1597
- validateGraph
5038
+ scanGeneratedArtifacts,
5039
+ scanProductionSource,
5040
+ validateGraph,
5041
+ watchProject
1598
5042
  };