@supacloud/compiler 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,383 @@
1
1
  // src/analyze.ts
2
- import { existsSync } from "node:fs";
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
3
13
  import { createHash } from "node:crypto";
4
- import { join, relative, sep } from "node:path";
5
- import {
6
- Node,
7
- Project,
8
- SyntaxKind
9
- } from "ts-morph";
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/util.ts
328
+ function camelName(token) {
329
+ const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
330
+ if (isConstantCase) {
331
+ return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
332
+ }
333
+ return token.charAt(0).toLowerCase() + token.slice(1);
334
+ }
335
+ function relativeImportPath(fromDir, toFile) {
336
+ const fromParts = fromDir.split("/").filter(Boolean);
337
+ const toParts = toFile.split("/").filter(Boolean);
338
+ let common = 0;
339
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
340
+ common += 1;
341
+ }
342
+ const ups = fromParts.length - common;
343
+ const downs = toParts.slice(common);
344
+ const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
345
+ const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
346
+ const joined = segments.join("/");
347
+ return joined.startsWith("..") ? joined : `./${joined}`;
348
+ }
349
+ var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
350
+ var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
351
+ function isRequestContextToken(token, tokenNames) {
352
+ return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
353
+ }
354
+ function isJobContextToken(token, tokenNames) {
355
+ return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
356
+ }
357
+ function joinRoutePaths(prefix, path) {
358
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
359
+ const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
360
+ return normalized;
361
+ }
362
+ function findClosestMatch(target, candidates) {
363
+ if (candidates.length === 0)
364
+ return;
365
+ if (candidates.length === 1)
366
+ return candidates[0];
367
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
368
+ const targetNorm = norm(target);
369
+ for (const c of candidates) {
370
+ if (norm(c) === targetNorm)
371
+ return c;
372
+ }
373
+ for (const c of candidates) {
374
+ if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
375
+ return c;
376
+ }
377
+ return candidates[0];
378
+ }
379
+
380
+ // src/analyze.ts
10
381
  var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
11
382
  var ROUTE_DECORATORS = {
12
383
  Get: "GET",
@@ -18,63 +389,118 @@ var ROUTE_DECORATORS = {
18
389
  Options: "OPTIONS"
19
390
  };
20
391
  var SCOPES = ["application", "request", "job"];
21
- async function analyzeProject(rootDir, include, cache) {
22
- let project;
23
- if (cache?.project) {
24
- project = cache.project;
25
- for (const sf of project.getSourceFiles()) {
26
- sf.refreshFromFileSystemSync();
27
- }
28
- } else {
29
- project = createProject(rootDir);
30
- const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
31
- project.addSourceFilesAtPaths(patterns);
32
- if (cache)
33
- cache.project = project;
34
- }
35
- const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
392
+ function isScope(value) {
393
+ return SCOPES.some((scope) => scope === value);
394
+ }
395
+ function nodeText(node) {
396
+ return node.getText(node.getSourceFile());
397
+ }
398
+ function lineOf(node) {
399
+ const sourceFile = node.getSourceFile();
400
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
401
+ }
402
+ function variableName(decl) {
403
+ return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
404
+ }
405
+ function propertyName(name) {
406
+ if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
407
+ return name.text;
408
+ if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
409
+ return name.text;
410
+ return nodeText(name);
411
+ }
412
+ function parameterName(param) {
413
+ return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
414
+ }
415
+ function decoratorsOf(node) {
416
+ return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
417
+ }
418
+ function decoratorArguments(dec) {
419
+ return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
420
+ }
421
+ function hasMethod(cls, name) {
422
+ return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
423
+ }
424
+ function hasDestroyHook(cls) {
425
+ return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
426
+ }
427
+ function descendantsOfKind(root, predicate) {
428
+ const result = [];
429
+ const visit = (node) => {
430
+ if (predicate(node))
431
+ result.push(node);
432
+ ts3.forEachChild(node, visit);
433
+ };
434
+ visit(root);
435
+ return result;
436
+ }
437
+ async function analyzeProject(rootDir, include, cache, changedPaths) {
438
+ const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
439
+ if (cache)
440
+ cache.programSession = session;
441
+ const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
442
+ const update = session.update(rootNames, changedPaths);
443
+ const program = update.program;
444
+ const checker = program.getTypeChecker();
445
+ 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));
36
446
  const ctx = {
37
447
  rootDir,
448
+ program,
449
+ checker,
38
450
  tokensByName: new Map,
39
451
  classesByName: new Map,
40
452
  diagnostics: []
41
453
  };
454
+ const nativeTraitFiles = new Map;
455
+ for (const diagnostic of session.getDiagnostics()) {
456
+ ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
457
+ }
458
+ for (const trait of session.getTraits()) {
459
+ const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
460
+ kinds.add(trait.kind);
461
+ nativeTraitFiles.set(trait.file, kinds);
462
+ }
42
463
  for (const sf of sourceFiles) {
43
464
  indexFile(sf, ctx);
44
465
  }
45
466
  const candidates = [];
46
467
  for (const sf of sourceFiles) {
47
- for (const cls of sf.getClasses()) {
48
- const moduleDec = findDecorator(cls, "Module");
49
- if (!moduleDec)
50
- continue;
51
- const options = decoratorObjectArg(moduleDec);
52
- if (!options)
53
- continue;
54
- candidates.push({
55
- node: cls,
56
- options,
57
- className: cls.getName() ?? "<anonymous>",
58
- file: sf.getFilePath(),
59
- line: cls.getStartLineNumber()
60
- });
468
+ const traits = nativeTraitFiles.get(sf.fileName);
469
+ if (!cache || traits?.has("module")) {
470
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
471
+ const moduleDec = findDecorator(cls, "Module");
472
+ if (!moduleDec)
473
+ continue;
474
+ const options = decoratorObjectArg(moduleDec);
475
+ if (!options)
476
+ continue;
477
+ candidates.push({
478
+ node: cls,
479
+ options,
480
+ className: cls.name?.text ?? "<anonymous>",
481
+ file: sf.fileName,
482
+ line: lineOf(cls)
483
+ });
484
+ }
61
485
  }
62
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
63
- if (call.getExpression().getText() !== "defineModule")
64
- continue;
65
- const parent = call.getParent();
66
- if (!parent || !Node.isVariableDeclaration(parent))
67
- continue;
68
- const arg = call.getArguments()[0];
69
- if (!arg || !Node.isObjectLiteralExpression(arg))
70
- continue;
71
- candidates.push({
72
- node: parent,
73
- options: arg,
74
- className: parent.getName(),
75
- file: sf.getFilePath(),
76
- line: parent.getStartLineNumber()
77
- });
486
+ if (!cache || traits?.has("defineModule")) {
487
+ for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
488
+ if (nodeText(call.expression) !== "defineModule")
489
+ continue;
490
+ const parent = call.parent;
491
+ if (!parent || !ts3.isVariableDeclaration(parent))
492
+ continue;
493
+ const arg = call.arguments[0];
494
+ if (!arg || !ts3.isObjectLiteralExpression(arg))
495
+ continue;
496
+ candidates.push({
497
+ node: parent,
498
+ options: arg,
499
+ className: variableName(parent),
500
+ file: sf.fileName,
501
+ line: lineOf(parent)
502
+ });
503
+ }
78
504
  }
79
505
  }
80
506
  const nameByNode = new Map;
@@ -87,8 +513,8 @@ async function analyzeProject(rootDir, include, cache) {
87
513
  if (cache) {
88
514
  const currentFileHashes = new Map;
89
515
  for (const sf of sourceFiles) {
90
- const rel = sourcePath(rootDir, sf.getFilePath());
91
- const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
516
+ const rel = sourcePath(rootDir, sf.fileName);
517
+ const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
92
518
  currentFileHashes.set(rel, hash);
93
519
  }
94
520
  const changedFiles = new Set;
@@ -104,7 +530,7 @@ async function analyzeProject(rootDir, include, cache) {
104
530
  }
105
531
  const modulesToKeep = new Map;
106
532
  const finalModules = [];
107
- const finalDiagnostics = [];
533
+ const finalDiagnostics = [...ctx.diagnostics];
108
534
  const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
109
535
  for (const [modName, entry] of cache.modules.entries()) {
110
536
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
@@ -126,14 +552,7 @@ async function analyzeProject(rootDir, include, cache) {
126
552
  const diagBefore = ctx.diagnostics.length;
127
553
  const parsed = parseModule(c, nameByNode, ctx);
128
554
  const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
129
- const ownedFiles = new Set;
130
- ownedFiles.add(parsed.file);
131
- for (const p of parsed.providers)
132
- if (p.file)
133
- ownedFiles.add(p.file);
134
- for (const ctrl of parsed.controllers)
135
- if (ctrl.file)
136
- ownedFiles.add(ctrl.file);
555
+ const ownedFiles = collectModuleSourceClosure(parsed, ctx);
137
556
  const fileHashes = {};
138
557
  for (const f of ownedFiles) {
139
558
  fileHashes[f] = currentFileHashes.get(f) ?? "";
@@ -160,6 +579,7 @@ async function analyzeProject(rootDir, include, cache) {
160
579
  } else {
161
580
  modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
162
581
  }
582
+ modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
163
583
  const allRegisteredClasses = new Set;
164
584
  const allRegisteredControllers = new Set;
165
585
  const allRegisteredCommands = new Set;
@@ -184,9 +604,9 @@ async function analyzeProject(rootDir, include, cache) {
184
604
  if (!allRegisteredClasses.has(name)) {
185
605
  const injectable = parseInjectableOptions(classInfo.decl, ctx);
186
606
  if (injectable?.providedIn === "root") {
187
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
607
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
188
608
  const file = sourcePath(ctx.rootDir, classInfo.file);
189
- const line = classInfo.decl.getStartLineNumber();
609
+ const line = lineOf(classInfo.decl);
190
610
  if (missing) {
191
611
  warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
192
612
  }
@@ -201,8 +621,9 @@ async function analyzeProject(rootDir, include, cache) {
201
621
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
202
622
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
203
623
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
624
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
204
625
  providedIn: "root",
205
- hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
626
+ hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
206
627
  exported: true,
207
628
  file,
208
629
  line,
@@ -213,8 +634,8 @@ async function analyzeProject(rootDir, include, cache) {
213
634
  if (!allRegisteredControllers.has(name)) {
214
635
  const controllerDec = findDecorator(classInfo.decl, "Controller");
215
636
  if (controllerDec) {
216
- const arg = controllerDec.getArguments()[0];
217
- const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
637
+ const arg = decoratorArguments(controllerDec)[0];
638
+ const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
218
639
  if (isStandalone) {
219
640
  const ctrl = parseController(classInfo.decl, ctx);
220
641
  if (ctrl)
@@ -228,13 +649,14 @@ async function analyzeProject(rootDir, include, cache) {
228
649
  const meta = decoratorObjectArg(commandDec);
229
650
  if (meta && booleanProp(meta, "standalone")) {
230
651
  standaloneCommands.push({
231
- className: classInfo.decl.getName() ?? name,
232
- name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
652
+ className: classInfo.decl.name?.text ?? name,
653
+ name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
233
654
  permission: stringLiteralProp(meta, "permission"),
234
655
  transaction: commandModeProp(meta, "transaction") ?? "none",
235
656
  audit: stringLiteralProp(meta, "audit"),
236
657
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
237
- standalone: true
658
+ standalone: true,
659
+ aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
238
660
  });
239
661
  }
240
662
  }
@@ -259,13 +681,13 @@ async function analyzeProject(rootDir, include, cache) {
259
681
  if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
260
682
  const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
261
683
  if (existingRoot) {
262
- existingRoot.providers.push(...rootProviders);
263
- existingRoot.controllers.push(...standaloneControllers);
264
- existingRoot.commands.push(...standaloneCommands);
265
- for (const p of rootProviders) {
266
- if (!existingRoot.exports.includes(p.token))
267
- existingRoot.exports.push(p.token);
268
- }
684
+ modules = modules.map((module) => module === existingRoot ? {
685
+ ...module,
686
+ providers: [...module.providers, ...rootProviders],
687
+ controllers: [...module.controllers, ...standaloneControllers],
688
+ commands: [...module.commands, ...standaloneCommands],
689
+ exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
690
+ } : module);
269
691
  } else {
270
692
  const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
271
693
  modules.unshift({
@@ -304,25 +726,72 @@ async function analyzeProject(rootDir, include, cache) {
304
726
  cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
305
727
  };
306
728
  }
307
- function createProject(rootDir) {
308
- const tsConfigFilePath = join(rootDir, "tsconfig.json");
309
- if (existsSync(tsConfigFilePath)) {
310
- return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
729
+ function collectModuleSourceClosure(module, ctx) {
730
+ const seeds = new Set;
731
+ const addRelativeModule = (path) => {
732
+ if (!path)
733
+ return;
734
+ const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
735
+ seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
736
+ };
737
+ addRelativeModule(module.file);
738
+ for (const provider of module.providers)
739
+ addRelativeModule(provider.importPath);
740
+ for (const controller of module.controllers)
741
+ addRelativeModule(controller.importPath);
742
+ const ownedFiles = new Set;
743
+ const queue = [...seeds];
744
+ while (queue.length > 0) {
745
+ const fileName = queue.shift();
746
+ if (!fileName)
747
+ continue;
748
+ const sourceFile = ctx.program.getSourceFile(fileName);
749
+ if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
750
+ continue;
751
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
752
+ if (ownedFiles.has(relativeFile))
753
+ continue;
754
+ ownedFiles.add(relativeFile);
755
+ for (const statement of sourceFile.statements) {
756
+ let moduleName;
757
+ if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
758
+ moduleName = statement.moduleSpecifier.text;
759
+ } else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
760
+ moduleName = statement.moduleSpecifier.text;
761
+ } else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
762
+ moduleName = statement.moduleReference.expression.text;
763
+ }
764
+ if (!moduleName || moduleName.startsWith("node:"))
765
+ continue;
766
+ const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
767
+ if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
768
+ queue.push(resolved);
769
+ }
311
770
  }
312
- return new Project({
313
- compilerOptions: { experimentalDecorators: true, allowJs: false }
314
- });
771
+ return ownedFiles;
772
+ }
773
+ function resolveSourcePath(rootDir, file) {
774
+ const normalized = file.replace(/\\/g, "/");
775
+ return resolvePath(rootDir, normalized);
776
+ }
777
+ function isProjectSourcePath(fileName, rootDir) {
778
+ const normalized = fileName.replace(/\\/g, "/");
779
+ const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
780
+ return normalized === root || normalized.startsWith(`${root}/`);
781
+ }
782
+ function isProjectSourceFile(sourceFile, rootDir) {
783
+ return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
315
784
  }
316
785
  function indexFile(sf, ctx) {
317
- for (const cls of sf.getClasses()) {
318
- const name = cls.getName();
786
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
787
+ const name = cls.name?.text;
319
788
  if (name && !ctx.classesByName.has(name)) {
320
- ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
789
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
321
790
  }
322
791
  }
323
- for (const statement of sf.getVariableStatements()) {
324
- for (const decl of statement.getDeclarations()) {
325
- const info = parseTokenVariable(decl, sf.getFilePath());
792
+ for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
793
+ for (const decl of statement.declarationList.declarations) {
794
+ const info = parseTokenVariable(decl, sf.fileName);
326
795
  if (info && !ctx.tokensByName.has(info.name)) {
327
796
  ctx.tokensByName.set(info.name, info);
328
797
  }
@@ -330,19 +799,19 @@ function indexFile(sf, ctx) {
330
799
  }
331
800
  }
332
801
  function parseTokenVariable(decl, file) {
333
- const init = decl.getInitializer();
334
- if (!init || !Node.isNewExpression(init))
802
+ const init = decl.initializer;
803
+ if (!init || !ts3.isNewExpression(init))
335
804
  return;
336
- if (init.getExpression().getText() !== "InjectionToken")
805
+ if (nodeText(init.expression) !== "InjectionToken")
337
806
  return;
338
- const [nameArg, optionsArg] = init.getArguments();
339
- const info = { name: decl.getName(), file, line: decl.getStartLineNumber() };
340
- if (nameArg && Node.isStringLiteral(nameArg)) {
341
- info.stringName = nameArg.getLiteralText();
807
+ const [nameArg, optionsArg] = init.arguments ?? [];
808
+ const info = { name: variableName(decl), file, line: lineOf(decl) };
809
+ if (nameArg && ts3.isStringLiteral(nameArg)) {
810
+ info.stringName = nameArg.text;
342
811
  }
343
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
812
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
344
813
  const scope = stringLiteralProp(optionsArg, "scope");
345
- if (scope && SCOPES.includes(scope)) {
814
+ if (scope && isScope(scope)) {
346
815
  info.scope = scope;
347
816
  }
348
817
  const providedIn = stringLiteralProp(optionsArg, "providedIn");
@@ -359,33 +828,76 @@ function parseTokenVariable(decl, file) {
359
828
  function parseModule(candidate, nameByNode, ctx) {
360
829
  const { options, className, file, line } = candidate;
361
830
  const name = nameByNode.get(candidate.node) ?? className;
362
- const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
831
+ const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
832
+ const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
363
833
  const imports = arrayProp(options, "imports").map((el) => {
364
834
  const unwrapped = unwrapForwardRef(el);
365
- const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
835
+ const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
366
836
  if (decl) {
367
837
  const known = nameByNode.get(decl);
368
838
  if (known)
369
839
  return known;
370
- if (Node.isClassDeclaration(decl)) {
840
+ if (ts3.isClassDeclaration(decl)) {
371
841
  const dec = findDecorator(decl, "Module");
372
842
  const decOptions = dec && decoratorObjectArg(dec);
373
843
  const decName = decOptions && stringLiteralProp(decOptions, "name");
374
- return decName ?? decl.getName() ?? el.getText();
844
+ return decName ?? decl.name?.text ?? nodeText(el);
375
845
  }
376
- if (Node.isVariableDeclaration(decl))
377
- return decl.getName();
846
+ if (ts3.isVariableDeclaration(decl))
847
+ return variableName(decl);
378
848
  }
379
- return el.getText();
849
+ return nodeText(el);
380
850
  }).filter((v, i, arr) => arr.indexOf(v) === i);
381
851
  const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
382
852
  const exportsSet = new Set(exports);
383
853
  const providers = [];
384
- for (const el of arrayProp(options, "providers")) {
854
+ for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
855
+ const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
856
+ if (parsedProviders) {
857
+ providers.push(...parsedProviders);
858
+ continue;
859
+ }
860
+ if (ts3.isCallExpression(el)) {
861
+ const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
862
+ warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
863
+ continue;
864
+ }
385
865
  const provider = parseProvider(el, exportsSet, ctx);
386
866
  if (provider)
387
867
  providers.push(provider);
388
868
  }
869
+ for (const el of arrayProp(options, "jobs")) {
870
+ if (!ts3.isIdentifier(el))
871
+ continue;
872
+ const decl = resolveDeclaration(el, ctx)[0];
873
+ if (!decl || !ts3.isClassDeclaration(decl))
874
+ continue;
875
+ const className2 = decl.name?.text ?? el.text;
876
+ const registeredProvider = providers.find((provider) => provider.token === className2 || provider.useClass === className2);
877
+ if (registeredProvider)
878
+ continue;
879
+ const deps = classDeps(decl, ctx);
880
+ const injectable = parseInjectableOptions(decl, ctx);
881
+ const scope = injectable?.scope ?? "job";
882
+ providers.push({
883
+ token: className2,
884
+ tokenKind: "class",
885
+ kind: "class",
886
+ useClass: className2,
887
+ scope,
888
+ deps: deps.deps,
889
+ optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
890
+ selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
891
+ skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
892
+ hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
893
+ functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
894
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
895
+ exported: exportsSet.has(className2),
896
+ file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
897
+ line: lineOf(decl),
898
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
899
+ });
900
+ }
389
901
  const controllers = [];
390
902
  for (const el of arrayProp(options, "controllers")) {
391
903
  const controller = parseController(el, ctx);
@@ -395,40 +907,74 @@ function parseModule(candidate, nameByNode, ctx) {
395
907
  const handlerClasses = [];
396
908
  const seenHandlers = new Set;
397
909
  const collectHandler = (expr) => {
398
- if (!Node.isIdentifier(expr))
910
+ if (!ts3.isIdentifier(expr))
399
911
  return;
400
- const decl = resolveDeclaration(expr)[0];
401
- if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
402
- seenHandlers.add(decl.getName() ?? "");
912
+ const decl = resolveDeclaration(expr, ctx)[0];
913
+ if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
914
+ seenHandlers.add(decl.name?.text ?? "");
403
915
  handlerClasses.push(decl);
404
916
  }
405
917
  };
406
918
  for (const el of arrayProp(options, "providers")) {
407
- if (Node.isIdentifier(el))
919
+ if (ts3.isIdentifier(el))
408
920
  collectHandler(el);
409
- if (Node.isObjectLiteralExpression(el)) {
921
+ if (ts3.isObjectLiteralExpression(el)) {
410
922
  const useClass = getProp(el, "useClass");
411
923
  if (useClass)
412
924
  collectHandler(useClass);
413
925
  }
414
926
  }
415
927
  arrayProp(options, "commands").forEach(collectHandler);
928
+ arrayProp(options, "jobs").forEach(collectHandler);
416
929
  arrayProp(options, "queries").forEach(collectHandler);
417
930
  const commands = [];
931
+ const jobs = [];
418
932
  const queries = [];
419
933
  for (const cls of handlerClasses) {
420
934
  const commandDec = findDecorator(cls, "Command");
421
935
  if (commandDec) {
422
936
  const meta = decoratorObjectArg(commandDec);
423
937
  if (meta) {
938
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
424
939
  commands.push({
425
- className: cls.getName() ?? "<anonymous>",
426
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
940
+ className: cls.name?.text ?? "<anonymous>",
941
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
427
942
  permission: stringLiteralProp(meta, "permission"),
428
943
  transaction: commandModeProp(meta, "transaction") ?? "none",
429
944
  audit: stringLiteralProp(meta, "audit"),
430
945
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
431
- standalone: booleanProp(meta, "standalone") || undefined
946
+ ...booleanProp(meta, "standalone") ? { standalone: true } : {},
947
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
948
+ });
949
+ }
950
+ }
951
+ const jobDec = findDecorator(cls, "Job");
952
+ if (jobDec) {
953
+ const meta = decoratorObjectArg(jobDec);
954
+ if (meta) {
955
+ const injectable = parseInjectableOptions(cls, ctx);
956
+ const className2 = cls.name?.text ?? "<anonymous>";
957
+ const provider = providers.find((candidate2) => candidate2.token === className2 || candidate2.useClass === className2);
958
+ const scope = provider?.scope ?? injectable?.scope ?? "job";
959
+ if (scope === "request") {
960
+ ctx.diagnostics.push({
961
+ severity: "error",
962
+ code: "invalid-job-scope",
963
+ message: `job ${className2} 不能使用 request scope;Job 只能使用 application 或 job scope`,
964
+ file: sourcePath(ctx.rootDir, cls.getSourceFile().fileName),
965
+ line: lineOf(cls),
966
+ suggestion: "移除 request scope,或改用 application/job scope。",
967
+ errorCode: "SC4007",
968
+ docsUrl: "https://supacloud.dev/errors/SC4007"
969
+ });
970
+ }
971
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${className2}`);
972
+ jobs.push({
973
+ className: className2,
974
+ name: stringLiteralProp(meta, "name") ?? className2,
975
+ serviceKey: camelName(provider?.token ?? className2),
976
+ scope,
977
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
432
978
  });
433
979
  }
434
980
  }
@@ -437,8 +983,8 @@ function parseModule(candidate, nameByNode, ctx) {
437
983
  const meta = decoratorObjectArg(queryDec);
438
984
  if (meta) {
439
985
  queries.push({
440
- className: cls.getName() ?? "<anonymous>",
441
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
986
+ className: cls.name?.text ?? "<anonymous>",
987
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
442
988
  });
443
989
  }
444
990
  }
@@ -453,7 +999,9 @@ function parseModule(candidate, nameByNode, ctx) {
453
999
  providers,
454
1000
  controllers,
455
1001
  commands,
1002
+ jobs,
456
1003
  queries,
1004
+ ...aspects.length > 0 ? { aspects } : {},
457
1005
  exports
458
1006
  };
459
1007
  }
@@ -462,14 +1010,14 @@ function commandModeProp(object, name) {
462
1010
  return value === "required" || value === "none" ? value : undefined;
463
1011
  }
464
1012
  function parseProvider(el, exportsSet, ctx) {
465
- const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
466
- const line = el.getStartLineNumber();
1013
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
1014
+ const line = lineOf(el);
467
1015
  const unwrappedEl = unwrapForwardRef(el);
468
- if (Node.isIdentifier(unwrappedEl)) {
469
- const decl = resolveDeclaration(unwrappedEl)[0];
470
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
471
- const className = cls?.getName() ?? unwrappedEl.getText();
472
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], missing: false };
1016
+ if (ts3.isIdentifier(unwrappedEl)) {
1017
+ const decl = resolveDeclaration(unwrappedEl, ctx)[0];
1018
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
1019
+ const className = cls?.name?.text ?? unwrappedEl.text;
1020
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
473
1021
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
474
1022
  if (missing) {
475
1023
  warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
@@ -485,15 +1033,16 @@ function parseProvider(el, exportsSet, ctx) {
485
1033
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
486
1034
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
487
1035
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1036
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
488
1037
  providedIn: injectable?.providedIn,
489
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1038
+ hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
490
1039
  exported: exportsSet.has(className),
491
1040
  file,
492
1041
  line,
493
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1042
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
494
1043
  };
495
1044
  }
496
- if (!Node.isObjectLiteralExpression(el))
1045
+ if (!ts3.isObjectLiteralExpression(el))
497
1046
  return;
498
1047
  const provideExpr = getProp(el, "provide");
499
1048
  if (!provideExpr)
@@ -508,26 +1057,36 @@ function parseProvider(el, exportsSet, ctx) {
508
1057
  const useExistingExpr = getProp(el, "useExisting");
509
1058
  if (useClassExpr) {
510
1059
  const unwrappedClass = unwrapForwardRef(useClassExpr);
511
- const decl = Node.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass)[0] : undefined;
512
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
513
- const useClass = cls?.getName() ?? unwrappedClass.getText();
1060
+ const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
1061
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
1062
+ const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
514
1063
  let deps = explicitDeps;
515
1064
  let optionalDeps = [];
516
1065
  let selfDeps = [];
517
1066
  let skipSelfDeps = [];
518
1067
  let hostDeps = [];
519
- if (deps.length === 0 && cls) {
1068
+ let functionalInjects = [];
1069
+ if (cls) {
520
1070
  const result = classDeps(cls, ctx);
521
- deps = result.deps;
522
- optionalDeps = result.optionalDeps;
523
- selfDeps = result.selfDeps;
524
- skipSelfDeps = result.skipSelfDeps;
525
- hostDeps = result.hostDeps;
1071
+ if (deps.length === 0) {
1072
+ deps = result.deps;
1073
+ optionalDeps = result.optionalDeps;
1074
+ selfDeps = result.selfDeps;
1075
+ skipSelfDeps = result.skipSelfDeps;
1076
+ hostDeps = result.hostDeps;
1077
+ } else {
1078
+ optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
1079
+ selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
1080
+ skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
1081
+ hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
1082
+ }
1083
+ functionalInjects = result.functionalInjects;
526
1084
  if (result.missing) {
527
1085
  warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
528
1086
  }
529
1087
  }
530
1088
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
1089
+ validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
531
1090
  return {
532
1091
  token,
533
1092
  tokenKind,
@@ -539,35 +1098,38 @@ function parseProvider(el, exportsSet, ctx) {
539
1098
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
540
1099
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
541
1100
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1101
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
542
1102
  multi: multi ?? undefined,
543
1103
  providedIn: injectable?.providedIn,
544
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1104
+ hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
545
1105
  exported: exportsSet.has(token),
546
1106
  file,
547
1107
  line,
548
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1108
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
549
1109
  };
550
1110
  }
551
1111
  if (useValueExpr) {
1112
+ validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
552
1113
  return {
553
1114
  token,
554
1115
  tokenKind,
555
1116
  kind: "value",
556
- useValueExpr: useValueExpr.getText(),
1117
+ useValueExpr: nodeText(useValueExpr),
557
1118
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
558
1119
  deps: [],
559
1120
  multi: multi ?? undefined,
560
1121
  exported: exportsSet.has(token),
561
1122
  file,
562
1123
  line,
563
- importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
1124
+ importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
564
1125
  };
565
1126
  }
566
1127
  if (useFactoryExpr) {
567
- const factoryName = Node.isIdentifier(useFactoryExpr) ? (() => {
568
- const decl = resolveDeclaration(useFactoryExpr)[0];
569
- return decl && (Node.isFunctionDeclaration(decl) || Node.isVariableDeclaration(decl)) ? decl.getName() ?? useFactoryExpr.getText() : useFactoryExpr.getText();
570
- })() : useFactoryExpr.getText();
1128
+ const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
1129
+ const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
1130
+ return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
1131
+ })() : nodeText(useFactoryExpr);
1132
+ validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
571
1133
  return {
572
1134
  token,
573
1135
  tokenKind,
@@ -579,11 +1141,12 @@ function parseProvider(el, exportsSet, ctx) {
579
1141
  exported: exportsSet.has(token),
580
1142
  file,
581
1143
  line,
582
- importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
1144
+ importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
583
1145
  };
584
1146
  }
585
1147
  if (useExistingExpr) {
586
1148
  const target = tokenNameOf(useExistingExpr, ctx).name;
1149
+ validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
587
1150
  return {
588
1151
  token,
589
1152
  tokenKind,
@@ -599,14 +1162,262 @@ function parseProvider(el, exportsSet, ctx) {
599
1162
  }
600
1163
  return;
601
1164
  }
1165
+ function expandProviderExpressions(expressions, ctx, seen = new Set) {
1166
+ const result = [];
1167
+ for (const expression of expressions) {
1168
+ if (ts3.isSpreadElement(expression)) {
1169
+ result.push(...expandProviderExpressions([expression.expression], ctx, seen));
1170
+ continue;
1171
+ }
1172
+ if (ts3.isIdentifier(expression)) {
1173
+ const declaration = resolveDeclaration(expression, ctx)[0];
1174
+ if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
1175
+ const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
1176
+ if (seen.has(key))
1177
+ continue;
1178
+ const initializer = declaration.initializer;
1179
+ if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
1180
+ const nested = initializer.arguments[0];
1181
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1182
+ seen.add(key);
1183
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1184
+ seen.delete(key);
1185
+ continue;
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+ if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
1191
+ const nested = expression.arguments[0];
1192
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1193
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1194
+ continue;
1195
+ }
1196
+ }
1197
+ result.push(expression);
1198
+ }
1199
+ return result;
1200
+ }
1201
+ function isProviderHelper(expression, name) {
1202
+ return nodeText(expression.expression).split(".").pop() === name;
1203
+ }
1204
+ function parseFunctionalProvider(expression, exportsSet, ctx) {
1205
+ if (!ts3.isCallExpression(expression))
1206
+ return;
1207
+ const helper = nodeText(expression.expression).split(".").pop();
1208
+ const args = expression.arguments;
1209
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1210
+ const line = lineOf(expression);
1211
+ if (helper === "provideToken") {
1212
+ const tokenExpr = args[0];
1213
+ const valueExpr = args[1];
1214
+ if (!tokenExpr || !valueExpr)
1215
+ return [];
1216
+ const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
1217
+ validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
1218
+ return [{
1219
+ token,
1220
+ tokenKind,
1221
+ kind: "value",
1222
+ useValueExpr: nodeText(valueExpr),
1223
+ scope: resolveScope({ tokenName: token }, ctx),
1224
+ deps: [],
1225
+ exported: exportsSet.has(token),
1226
+ file,
1227
+ line,
1228
+ importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
1229
+ }];
1230
+ }
1231
+ if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
1232
+ const initializer = args[0];
1233
+ if (!initializer)
1234
+ return [];
1235
+ const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
1236
+ return [{
1237
+ token,
1238
+ tokenKind: "injection-token",
1239
+ kind: "value",
1240
+ useValueExpr: nodeText(initializer),
1241
+ scope: "application",
1242
+ deps: [],
1243
+ multi: true,
1244
+ exported: false,
1245
+ file,
1246
+ line,
1247
+ importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
1248
+ }];
1249
+ }
1250
+ if (helper === "provideRouter") {
1251
+ const providers = [];
1252
+ const routes = args[0];
1253
+ if (routes) {
1254
+ providers.push({
1255
+ token: "ROUTE_CONFIG",
1256
+ tokenKind: "injection-token",
1257
+ kind: "value",
1258
+ useValueExpr: nodeText(routes),
1259
+ scope: "application",
1260
+ deps: [],
1261
+ exported: false,
1262
+ file,
1263
+ line,
1264
+ importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
1265
+ });
1266
+ }
1267
+ for (const feature of args.slice(1)) {
1268
+ if (!ts3.isCallExpression(feature))
1269
+ continue;
1270
+ const featureName = nodeText(feature.expression).split(".").pop();
1271
+ if (featureName === "withRouterConfig" && feature.arguments[0]) {
1272
+ providers.push({
1273
+ token: "ROUTER_CONFIGURATION",
1274
+ tokenKind: "injection-token",
1275
+ kind: "value",
1276
+ useValueExpr: nodeText(feature.arguments[0]),
1277
+ scope: "application",
1278
+ deps: [],
1279
+ exported: false,
1280
+ file,
1281
+ line
1282
+ });
1283
+ } else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
1284
+ const strategy = feature.arguments[0];
1285
+ const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
1286
+ providers.push({
1287
+ token: "TITLE_STRATEGY",
1288
+ tokenKind: "injection-token",
1289
+ kind: isClass ? "class" : "value",
1290
+ ...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
1291
+ scope: "application",
1292
+ deps: [],
1293
+ exported: false,
1294
+ file,
1295
+ line,
1296
+ importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
1297
+ });
1298
+ }
1299
+ }
1300
+ return providers;
1301
+ }
1302
+ if (helper === "provideHttpClient") {
1303
+ const providers = [{
1304
+ token: "HttpClient",
1305
+ tokenKind: "class",
1306
+ kind: "class",
1307
+ useClass: "HttpClient",
1308
+ scope: "application",
1309
+ deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1310
+ optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1311
+ exported: false,
1312
+ file,
1313
+ line,
1314
+ importModule: "@supacloud/app"
1315
+ }];
1316
+ for (const feature of args) {
1317
+ if (!ts3.isCallExpression(feature))
1318
+ continue;
1319
+ const featureName = nodeText(feature.expression).split(".").pop();
1320
+ if (featureName === "withInterceptors") {
1321
+ for (const interceptorArg of feature.arguments) {
1322
+ const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
1323
+ for (const value of values) {
1324
+ providers.push({
1325
+ token: "HTTP_INTERCEPTORS",
1326
+ tokenKind: "injection-token",
1327
+ kind: "value",
1328
+ useValueExpr: nodeText(value),
1329
+ scope: "application",
1330
+ deps: [],
1331
+ multi: true,
1332
+ exported: false,
1333
+ file,
1334
+ line,
1335
+ importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
1336
+ });
1337
+ }
1338
+ }
1339
+ } else if (featureName === "withFetch" && feature.arguments.length > 0) {
1340
+ warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
1341
+ }
1342
+ }
1343
+ return providers;
1344
+ }
1345
+ return;
1346
+ }
1347
+ function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
1348
+ const expected = providerTokenValueType(provideExpr, ctx);
1349
+ const actual = providerImplementationType(implementationExpr, kind, ctx);
1350
+ if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
1351
+ return;
1352
+ if (ctx.checker.isTypeAssignableTo(actual, expected))
1353
+ return;
1354
+ const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
1355
+ ctx.diagnostics.push({
1356
+ severity: "error",
1357
+ code: "provider-type-mismatch",
1358
+ message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
1359
+ file,
1360
+ line,
1361
+ errorCode: "SC2010",
1362
+ docsUrl: "https://supacloud.dev/errors/SC2010"
1363
+ });
1364
+ }
1365
+ function providerTokenValueType(expr, ctx) {
1366
+ const type = ctx.checker.getTypeAtLocation(expr);
1367
+ const typeArguments = typeArgumentsOf(type, ctx);
1368
+ if (typeArguments.length > 0)
1369
+ return typeArguments[0];
1370
+ if (ts3.isIdentifier(expr)) {
1371
+ const declaration = resolveDeclaration(expr, ctx)[0];
1372
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1373
+ return declaredClassType(declaration, ctx);
1374
+ }
1375
+ }
1376
+ return;
1377
+ }
1378
+ function providerImplementationType(expr, kind, ctx) {
1379
+ if (kind === "class" || kind === "existing") {
1380
+ if (ts3.isIdentifier(expr)) {
1381
+ const declaration = resolveDeclaration(expr, ctx)[0];
1382
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1383
+ return declaredClassType(declaration, ctx);
1384
+ }
1385
+ }
1386
+ const type = ctx.checker.getTypeAtLocation(expr);
1387
+ const typeArguments = typeArgumentsOf(type, ctx);
1388
+ return typeArguments.length > 0 ? typeArguments[0] : undefined;
1389
+ }
1390
+ if (kind === "factory") {
1391
+ const type = ctx.checker.getTypeAtLocation(expr);
1392
+ const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
1393
+ return signature?.getReturnType();
1394
+ }
1395
+ return ctx.checker.getTypeAtLocation(expr);
1396
+ }
1397
+ function declaredClassType(declaration, ctx) {
1398
+ const name = declaration.name;
1399
+ if (!name)
1400
+ return;
1401
+ const symbol = ctx.checker.getSymbolAtLocation(name);
1402
+ return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
1403
+ }
1404
+ function typeArgumentsOf(type, ctx) {
1405
+ return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
1406
+ }
1407
+ function isTypeReference(type) {
1408
+ return "target" in type;
1409
+ }
1410
+ function isUnknownOrAny(type) {
1411
+ return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
1412
+ }
602
1413
  function parseController(input, ctx) {
603
1414
  let decl;
604
- if (Node.isClassDeclaration(input)) {
1415
+ if (ts3.isClassDeclaration(input)) {
605
1416
  decl = input;
606
1417
  } else {
607
1418
  const unwrapped = unwrapForwardRef(input);
608
- const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
609
- if (resolved && Node.isClassDeclaration(resolved)) {
1419
+ const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
1420
+ if (resolved && ts3.isClassDeclaration(resolved)) {
610
1421
  decl = resolved;
611
1422
  }
612
1423
  }
@@ -617,46 +1428,46 @@ function parseController(input, ctx) {
617
1428
  return;
618
1429
  let path = "/";
619
1430
  let standalone;
620
- const pathArg = controllerDec.getArguments()[0];
1431
+ const pathArg = decoratorArguments(controllerDec)[0];
621
1432
  if (pathArg) {
622
- if (Node.isStringLiteral(pathArg)) {
623
- path = pathArg.getLiteralText();
624
- } else if (Node.isObjectLiteralExpression(pathArg)) {
1433
+ if (ts3.isStringLiteral(pathArg)) {
1434
+ path = pathArg.text;
1435
+ } else if (ts3.isObjectLiteralExpression(pathArg)) {
625
1436
  const p = stringLiteralProp(pathArg, "path");
626
1437
  if (p)
627
1438
  path = p;
628
1439
  standalone = booleanProp(pathArg, "standalone");
629
1440
  }
630
1441
  }
631
- const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
632
- const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1442
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
1443
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
633
1444
  if (missing) {
634
- warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
1445
+ warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
635
1446
  }
636
1447
  const injectable = parseInjectableOptions(decl, ctx);
637
1448
  const routes = [];
638
1449
  const schemaImports = {};
639
1450
  const classGuards = [];
640
- for (const dec of decl.getDecorators()) {
641
- if (decoratorName(dec) === "UseGuards") {
642
- for (const gArg of dec.getArguments()) {
643
- classGuards.push(tokenText(gArg));
1451
+ for (const dec of decoratorsOf(decl)) {
1452
+ if (decoratorName2(dec) === "UseGuards") {
1453
+ for (const gArg of decoratorArguments(dec)) {
1454
+ classGuards.push(tokenText(gArg, ctx));
644
1455
  }
645
1456
  }
646
1457
  }
647
- for (const method of decl.getMethods()) {
648
- for (const dec of method.getDecorators()) {
649
- const name = decoratorName(dec);
1458
+ for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
1459
+ for (const dec of decoratorsOf(method)) {
1460
+ const name = decoratorName2(dec);
650
1461
  const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
651
1462
  if (!httpMethod)
652
1463
  continue;
653
- const args = dec.getArguments();
1464
+ const args = decoratorArguments(dec);
654
1465
  const pathArg2 = args[0];
655
- const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
1466
+ const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
656
1467
  const route = {
657
1468
  method: httpMethod,
658
1469
  path: routePath,
659
- handler: method.getName()
1470
+ handler: propertyName(method.name)
660
1471
  };
661
1472
  const pathParams = [];
662
1473
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
@@ -674,13 +1485,13 @@ function parseController(input, ctx) {
674
1485
  const queryDefaults = {};
675
1486
  let hasBodyBinding = false;
676
1487
  const handlerParams = [];
677
- for (const p of method.getParameters()) {
678
- const pName = p.getName();
1488
+ for (const p of method.parameters) {
1489
+ const pName = parameterName(p);
679
1490
  let hasBindingDecorator = false;
680
1491
  let paramNode;
681
- for (const pDec of p.getDecorators()) {
682
- const dName = decoratorName(pDec);
683
- const dArgs = pDec.getArguments();
1492
+ for (const pDec of decoratorsOf(p)) {
1493
+ const dName = decoratorName2(pDec);
1494
+ const dArgs = decoratorArguments(pDec);
684
1495
  if (dName === "Param") {
685
1496
  hasBindingDecorator = true;
686
1497
  const parsed = parseBindingOptions(dArgs, pName);
@@ -722,7 +1533,7 @@ function parseController(input, ctx) {
722
1533
  }
723
1534
  if (!hasBindingDecorator && pathParams.includes(pName)) {
724
1535
  paramBindings.push(pName);
725
- const typeText = p.getType().getText();
1536
+ const typeText = p.type ? nodeText(p.type) : "";
726
1537
  let inferredTransform;
727
1538
  if (typeText === "number") {
728
1539
  paramTransforms[pName] = "number";
@@ -765,37 +1576,37 @@ function parseController(input, ctx) {
765
1576
  route.handlerParams = handlerParams;
766
1577
  const routeGuards = [...classGuards];
767
1578
  const routeCanDeactivate = [];
768
- for (const mDec of method.getDecorators()) {
769
- const dName = decoratorName(mDec);
770
- const mArgs = mDec.getArguments();
1579
+ for (const mDec of decoratorsOf(method)) {
1580
+ const dName = decoratorName2(mDec);
1581
+ const mArgs = decoratorArguments(mDec);
771
1582
  if (dName === "UseGuards") {
772
1583
  for (const gArg of mArgs) {
773
- routeGuards.push(tokenText(gArg));
1584
+ routeGuards.push(tokenText(gArg, ctx));
774
1585
  }
775
1586
  } else if (dName === "CanDeactivate") {
776
1587
  for (const gArg of mArgs) {
777
- routeCanDeactivate.push(tokenText(gArg));
1588
+ routeCanDeactivate.push(tokenText(gArg, ctx));
778
1589
  }
779
1590
  } else if (dName === "Title") {
780
1591
  const tArg = mArgs[0];
781
- if (tArg && Node.isStringLiteral(tArg)) {
782
- route.title = tArg.getLiteralText();
1592
+ if (tArg && ts3.isStringLiteral(tArg)) {
1593
+ route.title = tArg.text;
783
1594
  }
784
1595
  } else if (dName === "Data") {
785
1596
  const dArg = mArgs[0];
786
- if (dArg && Node.isObjectLiteralExpression(dArg)) {
1597
+ if (dArg && ts3.isObjectLiteralExpression(dArg)) {
787
1598
  route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
788
1599
  }
789
1600
  } else if (dName === "Resolve") {
790
1601
  const rArg = mArgs[0];
791
- if (rArg && Node.isObjectLiteralExpression(rArg)) {
1602
+ if (rArg && ts3.isObjectLiteralExpression(rArg)) {
792
1603
  const resolvers = route.resolvers ?? {};
793
- for (const prop of rArg.getProperties()) {
794
- if (Node.isPropertyAssignment(prop)) {
795
- const rName = prop.getName();
796
- const init = prop.getInitializer();
1604
+ for (const prop of rArg.properties) {
1605
+ if (ts3.isPropertyAssignment(prop)) {
1606
+ const rName = propertyName(prop.name);
1607
+ const init = prop.initializer;
797
1608
  if (init)
798
- resolvers[rName] = tokenText(init);
1609
+ resolvers[rName] = tokenText(init, ctx);
799
1610
  }
800
1611
  }
801
1612
  if (Object.keys(resolvers).length > 0) {
@@ -805,52 +1616,52 @@ function parseController(input, ctx) {
805
1616
  }
806
1617
  }
807
1618
  const optionsArg = args[1];
808
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
1619
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
809
1620
  for (const field of ["body", "params", "query", "response"]) {
810
1621
  const schemaExpr = getProp(optionsArg, field);
811
- if (schemaExpr && Node.isIdentifier(schemaExpr)) {
812
- route[field] = schemaExpr.getText();
1622
+ if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
1623
+ route[field] = nodeText(schemaExpr);
813
1624
  const importPath = importPathOf(schemaExpr, ctx);
814
1625
  if (importPath)
815
- schemaImports[schemaExpr.getText()] = importPath;
1626
+ schemaImports[schemaExpr.text] = importPath;
816
1627
  }
817
1628
  }
818
1629
  const commandExpr = getProp(optionsArg, "command");
819
- if (commandExpr && Node.isIdentifier(commandExpr)) {
820
- const commandDecl = resolveDeclaration(commandExpr)[0];
821
- route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
1630
+ if (commandExpr && ts3.isIdentifier(commandExpr)) {
1631
+ const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
1632
+ route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
822
1633
  }
823
1634
  const guardsExpr = getProp(optionsArg, "guards");
824
- if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
825
- for (const el of guardsExpr.getElements()) {
826
- routeGuards.push(tokenText(el));
1635
+ if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
1636
+ for (const el of guardsExpr.elements) {
1637
+ routeGuards.push(tokenText(el, ctx));
827
1638
  }
828
1639
  }
829
1640
  const canMatchExpr = getProp(optionsArg, "canMatch");
830
- if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
1641
+ if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
831
1642
  const canMatchList = [];
832
- for (const el of canMatchExpr.getElements()) {
833
- canMatchList.push(tokenText(el));
1643
+ for (const el of canMatchExpr.elements) {
1644
+ canMatchList.push(tokenText(el, ctx));
834
1645
  }
835
1646
  if (canMatchList.length > 0) {
836
1647
  route.canMatch = canMatchList;
837
1648
  }
838
1649
  }
839
1650
  const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
840
- if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
841
- for (const el of canDeactivateExpr.getElements()) {
842
- routeCanDeactivate.push(tokenText(el));
1651
+ if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
1652
+ for (const el of canDeactivateExpr.elements) {
1653
+ routeCanDeactivate.push(tokenText(el, ctx));
843
1654
  }
844
1655
  }
845
1656
  const resolversExpr = getProp(optionsArg, "resolvers");
846
- if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
1657
+ if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
847
1658
  const resolvers = {};
848
- for (const prop of resolversExpr.getProperties()) {
849
- if (Node.isPropertyAssignment(prop)) {
850
- const rName = prop.getName();
851
- const init = prop.getInitializer();
1659
+ for (const prop of resolversExpr.properties) {
1660
+ if (ts3.isPropertyAssignment(prop)) {
1661
+ const rName = propertyName(prop.name);
1662
+ const init = prop.initializer;
852
1663
  if (init)
853
- resolvers[rName] = tokenText(init);
1664
+ resolvers[rName] = tokenText(init, ctx);
854
1665
  }
855
1666
  }
856
1667
  if (Object.keys(resolvers).length > 0) {
@@ -858,24 +1669,27 @@ function parseController(input, ctx) {
858
1669
  }
859
1670
  }
860
1671
  const redirectToExpr = getProp(optionsArg, "redirectTo");
861
- if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
862
- route.redirectTo = redirectToExpr.getLiteralText();
1672
+ if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
1673
+ route.redirectTo = redirectToExpr.text;
863
1674
  }
864
1675
  const pathMatchExpr = getProp(optionsArg, "pathMatch");
865
- if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
866
- const val = pathMatchExpr.getLiteralText();
1676
+ if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
1677
+ const val = pathMatchExpr.text;
867
1678
  if (val === "full" || val === "prefix") {
868
1679
  route.pathMatch = val;
869
1680
  }
870
1681
  }
871
1682
  const titleExpr = getProp(optionsArg, "title");
872
- if (titleExpr && Node.isStringLiteral(titleExpr)) {
873
- route.title = titleExpr.getLiteralText();
1683
+ if (titleExpr && ts3.isStringLiteral(titleExpr)) {
1684
+ route.title = titleExpr.text;
874
1685
  }
875
1686
  const dataExpr = getProp(optionsArg, "data");
876
- if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
1687
+ if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
877
1688
  route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
878
1689
  }
1690
+ const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
1691
+ if (aspects.length > 0)
1692
+ route.aspects = aspects;
879
1693
  }
880
1694
  if (routeGuards.length > 0) {
881
1695
  route.guards = routeGuards;
@@ -887,46 +1701,40 @@ function parseController(input, ctx) {
887
1701
  }
888
1702
  }
889
1703
  return {
890
- className: decl.getName() ?? "<anonymous>",
1704
+ className: decl.name?.text ?? "<anonymous>",
891
1705
  path,
892
1706
  scope: injectable?.scope ?? "request",
893
1707
  deps,
1708
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
894
1709
  optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
895
1710
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
896
1711
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1712
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1713
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
897
1714
  standalone: standalone || undefined,
898
1715
  routes,
899
1716
  file,
900
- importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
1717
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
901
1718
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
902
1719
  };
903
1720
  }
904
1721
  function classDeps(cls, ctx) {
905
1722
  const injectable = parseInjectableOptions(cls, ctx);
906
- if (injectable?.deps) {
907
- return {
908
- deps: injectable.deps,
909
- optionalDeps: [],
910
- selfDeps: [],
911
- skipSelfDeps: [],
912
- hostDeps: [],
913
- missing: false
914
- };
915
- }
916
- const ctor = cls.getConstructors()[0];
917
- const deps = [];
1723
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1724
+ const deps = injectable?.deps ? [...injectable.deps] : [];
918
1725
  const optionalDeps = [];
919
1726
  const selfDeps = [];
920
1727
  const skipSelfDeps = [];
921
1728
  const hostDeps = [];
1729
+ const functionalInjects = [];
922
1730
  let missing = false;
923
- if (ctor && ctor.getParameters().length > 0) {
924
- const injectParams = parseInjectParams(cls);
1731
+ if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
1732
+ const injectParams = parseInjectParams(cls, ctx);
925
1733
  const optionalIndices = parseOptionalParams(cls);
926
1734
  const selfIndices = parseModifierParams(cls, "Self");
927
1735
  const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
928
1736
  const hostIndices = parseModifierParams(cls, "Host");
929
- ctor.getParameters().forEach((param, index) => {
1737
+ ctor.parameters.forEach((param, index) => {
930
1738
  const isOptional = optionalIndices.has(index);
931
1739
  const injected = injectParams.get(index);
932
1740
  const tokenName = injected ?? paramTypeTokenName(param, ctx);
@@ -946,47 +1754,62 @@ function classDeps(cls, ctx) {
946
1754
  }
947
1755
  });
948
1756
  }
949
- for (const prop of cls.getProperties()) {
950
- const init = prop.getInitializer();
951
- if (init && Node.isCallExpression(init)) {
952
- const callName = init.getExpression().getText().split(".").pop();
1757
+ for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
1758
+ const init = prop.initializer;
1759
+ if (init && ts3.isCallExpression(init)) {
1760
+ const callName = nodeText(init.expression).split(".").pop();
953
1761
  if (callName === "inject") {
954
- const [tokenArg, optionsArg] = init.getArguments();
1762
+ const [tokenArg, optionsArg] = init.arguments;
955
1763
  if (tokenArg) {
956
- const tokenName = tokenText(tokenArg);
957
- if (tokenName) {
958
- if (!deps.includes(tokenName))
959
- deps.push(tokenName);
960
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
961
- const isOptional = booleanProp(optionsArg, "optional");
962
- if (isOptional && !optionalDeps.includes(tokenName)) {
963
- optionalDeps.push(tokenName);
964
- }
965
- const isSelf = booleanProp(optionsArg, "self");
966
- if (isSelf && !selfDeps.includes(tokenName)) {
967
- selfDeps.push(tokenName);
968
- }
969
- const isSkipSelf = booleanProp(optionsArg, "skipSelf");
970
- if (isSkipSelf && !skipSelfDeps.includes(tokenName)) {
971
- skipSelfDeps.push(tokenName);
972
- }
973
- const isHost = booleanProp(optionsArg, "host");
974
- if (isHost && !hostDeps.includes(tokenName)) {
975
- hostDeps.push(tokenName);
976
- }
977
- }
1764
+ const tokenName = tokenText(tokenArg, ctx);
1765
+ const unwrappedToken = unwrapForwardRef(tokenArg);
1766
+ const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
1767
+ if (!known) {
1768
+ missing = true;
1769
+ continue;
1770
+ }
1771
+ if (!deps.includes(tokenName))
1772
+ deps.push(tokenName);
1773
+ const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
1774
+ optional: booleanProp(optionsArg, "optional") ?? false,
1775
+ self: booleanProp(optionsArg, "self") ?? false,
1776
+ skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
1777
+ host: booleanProp(optionsArg, "host") ?? false
1778
+ } : { optional: false, self: false, skipSelf: false, host: false };
1779
+ if (options.optional && !optionalDeps.includes(tokenName))
1780
+ optionalDeps.push(tokenName);
1781
+ if (options.self && !selfDeps.includes(tokenName))
1782
+ selfDeps.push(tokenName);
1783
+ if (options.skipSelf && !skipSelfDeps.includes(tokenName))
1784
+ skipSelfDeps.push(tokenName);
1785
+ if (options.host && !hostDeps.includes(tokenName))
1786
+ hostDeps.push(tokenName);
1787
+ if (!functionalInjects.some((entry) => entry.token === tokenName)) {
1788
+ functionalInjects.push({
1789
+ token: tokenName,
1790
+ expression: nodeText(unwrappedToken),
1791
+ importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
1792
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1793
+ return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
1794
+ })() : undefined,
1795
+ importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
1796
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1797
+ return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
1798
+ })() : undefined,
1799
+ ...options
1800
+ });
978
1801
  }
979
1802
  }
980
1803
  }
981
1804
  }
982
1805
  }
983
- return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
1806
+ return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
984
1807
  }
985
1808
  function paramTypeTokenName(param, ctx) {
986
- const typeNode = param.getTypeNode();
1809
+ const typeNode = param.type;
987
1810
  if (!typeNode)
988
1811
  return;
989
- const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
1812
+ const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
990
1813
  if (ctx.classesByName.has(text))
991
1814
  return text;
992
1815
  if (ctx.tokensByName.has(text))
@@ -1004,63 +1827,63 @@ function parseInjectableOptions(cls, ctx) {
1004
1827
  const providedIn = stringLiteralProp(obj, "providedIn");
1005
1828
  const depsExpr = getProp(obj, "deps");
1006
1829
  return {
1007
- scope: scope && SCOPES.includes(scope) ? scope : undefined,
1830
+ scope: scope && isScope(scope) ? scope : undefined,
1008
1831
  providedIn: providedIn === "root" ? "root" : undefined,
1009
- deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
1832
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
1010
1833
  };
1011
1834
  }
1012
- function parseInjectParams(cls) {
1835
+ function parseInjectParams(cls, ctx) {
1013
1836
  const result = new Map;
1014
- const ctor = cls.getConstructors()[0];
1837
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1015
1838
  if (!ctor)
1016
1839
  return result;
1017
- ctor.getParameters().forEach((param, index) => {
1018
- for (const dec of param.getDecorators()) {
1019
- if (decoratorName(dec) !== "Inject")
1840
+ ctor.parameters.forEach((param, index) => {
1841
+ for (const dec of decoratorsOf(param)) {
1842
+ if (decoratorName2(dec) !== "Inject")
1020
1843
  continue;
1021
- const arg = dec.getArguments()[0];
1844
+ const arg = decoratorArguments(dec)[0];
1022
1845
  if (arg)
1023
- result.set(index, tokenText(arg));
1846
+ result.set(index, tokenText(arg, ctx));
1024
1847
  }
1025
1848
  });
1026
1849
  return result;
1027
1850
  }
1028
1851
  function parseOptionalParams(cls) {
1029
1852
  const result = new Set;
1030
- const ctor = cls.getConstructors()[0];
1853
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1031
1854
  if (!ctor)
1032
1855
  return result;
1033
- ctor.getParameters().forEach((param, index) => {
1034
- for (const dec of param.getDecorators()) {
1035
- if (decoratorName(dec) === "Optional")
1856
+ ctor.parameters.forEach((param, index) => {
1857
+ for (const dec of decoratorsOf(param)) {
1858
+ if (decoratorName2(dec) === "Optional")
1036
1859
  result.add(index);
1037
1860
  }
1038
- if (param.hasQuestionToken())
1861
+ if (param.questionToken)
1039
1862
  result.add(index);
1040
1863
  });
1041
1864
  return result;
1042
1865
  }
1043
1866
  function parseModifierParams(cls, modifierName) {
1044
1867
  const result = new Set;
1045
- const ctor = cls.getConstructors()[0];
1868
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1046
1869
  if (!ctor)
1047
1870
  return result;
1048
- ctor.getParameters().forEach((param, index) => {
1049
- for (const dec of param.getDecorators()) {
1050
- if (decoratorName(dec) === modifierName)
1871
+ ctor.parameters.forEach((param, index) => {
1872
+ for (const dec of decoratorsOf(param)) {
1873
+ if (decoratorName2(dec) === modifierName)
1051
1874
  result.add(index);
1052
1875
  }
1053
1876
  });
1054
1877
  return result;
1055
1878
  }
1056
1879
  function unwrapForwardRef(expr) {
1057
- if (Node.isCallExpression(expr)) {
1058
- const exprText = expr.getExpression().getText();
1880
+ if (ts3.isCallExpression(expr)) {
1881
+ const exprText = nodeText(expr.expression);
1059
1882
  if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
1060
- const arg = expr.getArguments()[0];
1061
- if (arg && (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg))) {
1062
- const body = arg.getBody();
1063
- if (body && Node.isExpression(body)) {
1883
+ const arg = expr.arguments[0];
1884
+ if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
1885
+ const body = arg.body;
1886
+ if (body && ts3.isExpression(body)) {
1064
1887
  return unwrapForwardRef(body);
1065
1888
  }
1066
1889
  }
@@ -1068,18 +1891,18 @@ function unwrapForwardRef(expr) {
1068
1891
  }
1069
1892
  return expr;
1070
1893
  }
1071
- function tokenText(expr) {
1894
+ function tokenText(expr, ctx) {
1072
1895
  const unwrapped = unwrapForwardRef(expr);
1073
- if (Node.isStringLiteral(unwrapped))
1074
- return unwrapped.getLiteralText();
1075
- if (Node.isIdentifier(unwrapped)) {
1076
- const decl = resolveDeclaration(unwrapped)[0];
1077
- if (decl && Node.isClassDeclaration(decl))
1078
- return decl.getName() ?? unwrapped.getText();
1079
- if (decl && Node.isVariableDeclaration(decl))
1080
- return decl.getName();
1896
+ if (ts3.isStringLiteral(unwrapped))
1897
+ return unwrapped.text;
1898
+ if (ts3.isIdentifier(unwrapped)) {
1899
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1900
+ if (decl && ts3.isClassDeclaration(decl))
1901
+ return decl.name?.text ?? unwrapped.text;
1902
+ if (decl && ts3.isVariableDeclaration(decl))
1903
+ return variableName(decl);
1081
1904
  }
1082
- return unwrapped.getText();
1905
+ return nodeText(unwrapped);
1083
1906
  }
1084
1907
  function resolveScope(input, ctx) {
1085
1908
  if (input.explicit)
@@ -1096,98 +1919,181 @@ function resolveScope(input, ctx) {
1096
1919
  }
1097
1920
  function tokenNameOf(expr, ctx) {
1098
1921
  const unwrapped = unwrapForwardRef(expr);
1099
- if (Node.isIdentifier(unwrapped)) {
1100
- const decl = resolveDeclaration(unwrapped)[0];
1101
- if (decl && Node.isClassDeclaration(decl)) {
1102
- return { name: decl.getName() ?? expr.getText(), kind: "class" };
1922
+ if (ts3.isIdentifier(unwrapped)) {
1923
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1924
+ if (decl && ts3.isClassDeclaration(decl)) {
1925
+ return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
1103
1926
  }
1104
- if (decl && Node.isVariableDeclaration(decl)) {
1105
- const name = decl.getName();
1927
+ if (decl && ts3.isVariableDeclaration(decl)) {
1928
+ const name = variableName(decl);
1106
1929
  return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
1107
1930
  }
1108
- if (ctx.tokensByName.has(expr.getText())) {
1109
- return { name: expr.getText(), kind: "injection-token" };
1931
+ if (ctx.tokensByName.has(unwrapped.text)) {
1932
+ return { name: unwrapped.text, kind: "injection-token" };
1110
1933
  }
1111
1934
  }
1112
- return { name: expr.getText(), kind: "class" };
1935
+ return { name: nodeText(expr), kind: "class" };
1113
1936
  }
1114
- function resolveDeclaration(id) {
1115
- let symbol = id.getSymbol();
1937
+ function resolveDeclaration(id, ctx) {
1938
+ let symbol = ctx.checker.getSymbolAtLocation(id);
1116
1939
  if (!symbol)
1117
1940
  return [];
1118
- let declarations = symbol.getDeclarations();
1941
+ let declarations = symbol.declarations ?? [];
1119
1942
  for (let guard = 0;guard < 4; guard += 1) {
1120
- const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
1943
+ const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
1121
1944
  if (!isAlias)
1122
1945
  break;
1123
- const aliased = symbol.getAliasedSymbol();
1124
- if (!aliased)
1946
+ if (!(symbol.flags & ts3.SymbolFlags.Alias))
1125
1947
  break;
1948
+ const aliased = ctx.checker.getAliasedSymbol(symbol);
1126
1949
  symbol = aliased;
1127
- declarations = aliased.getDeclarations();
1950
+ declarations = aliased.declarations ?? [];
1128
1951
  }
1129
1952
  return declarations;
1130
1953
  }
1131
1954
  function importPathOf(id, ctx) {
1132
- const symbol = id.getSymbol();
1133
- const first = symbol?.getDeclarations()[0];
1134
- if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
1135
- const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
1136
- const target = importDecl?.getModuleSpecifierSourceFile();
1137
- if (target)
1138
- return modulePath(ctx.rootDir, target.getFilePath());
1139
- }
1140
- const decl = resolveDeclaration(id)[0];
1955
+ const decl = resolveDeclaration(id, ctx)[0];
1141
1956
  if (decl)
1142
- return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1957
+ return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
1958
+ return;
1959
+ }
1960
+ function importModuleOf(id, ctx) {
1961
+ const symbol = ctx.checker.getSymbolAtLocation(id);
1962
+ const declarations = symbol?.declarations ?? [];
1963
+ for (const declaration of declarations) {
1964
+ let current = declaration;
1965
+ while (current) {
1966
+ if (ts3.isImportDeclaration(current)) {
1967
+ const moduleSpecifier = current.moduleSpecifier;
1968
+ return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
1969
+ }
1970
+ current = current.parent;
1971
+ }
1972
+ }
1143
1973
  return;
1144
1974
  }
1145
1975
  function findDecorator(cls, name) {
1146
- return cls.getDecorators().find((dec) => decoratorName(dec) === name);
1976
+ return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
1147
1977
  }
1148
- function decoratorName(dec) {
1149
- const expr = dec.getExpression();
1150
- if (Node.isCallExpression(expr)) {
1151
- return expr.getExpression().getText().split(".").pop();
1978
+ function decoratorName2(dec) {
1979
+ const expr = dec.expression;
1980
+ if (ts3.isCallExpression(expr)) {
1981
+ return nodeText(expr.expression).split(".").pop();
1152
1982
  }
1153
- if (Node.isIdentifier(expr))
1154
- return expr.getText();
1983
+ if (ts3.isIdentifier(expr))
1984
+ return expr.text;
1155
1985
  return;
1156
1986
  }
1157
1987
  function decoratorObjectArg(dec) {
1158
- const expr = dec.getExpression();
1159
- if (!Node.isCallExpression(expr))
1988
+ const expr = dec.expression;
1989
+ if (!ts3.isCallExpression(expr))
1160
1990
  return;
1161
- const arg = expr.getArguments()[0];
1162
- return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
1991
+ const arg = expr.arguments[0];
1992
+ return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
1163
1993
  }
1164
1994
  function getProp(obj, name) {
1165
- const prop = obj.getProperty(name);
1166
- if (prop && Node.isPropertyAssignment(prop))
1167
- return prop.getInitializer();
1995
+ const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
1996
+ if (!prop)
1997
+ return;
1998
+ if (ts3.isPropertyAssignment(prop))
1999
+ return prop.initializer;
2000
+ if (ts3.isShorthandPropertyAssignment(prop))
2001
+ return prop.name;
1168
2002
  return;
1169
2003
  }
2004
+ function toCompilerDiagnostic(diagnostic, rootDir) {
2005
+ const file = diagnostic.file;
2006
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
2007
+ return {
2008
+ severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
2009
+ code: `typescript-${diagnostic.code}`,
2010
+ errorCode: `TS${diagnostic.code}`,
2011
+ message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
2012
+ `),
2013
+ file: file ? sourcePath(rootDir, file.fileName) : undefined,
2014
+ line: position ? position.line + 1 : undefined
2015
+ };
2016
+ }
1170
2017
  function stringLiteralProp(obj, name) {
1171
2018
  const expr = getProp(obj, name);
1172
- return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
2019
+ return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
1173
2020
  }
1174
2021
  function arrayProp(obj, name) {
1175
2022
  const expr = getProp(obj, name);
1176
- return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
2023
+ return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
2024
+ }
2025
+ function parseAspectRefs(expression, ctx, owner) {
2026
+ if (!expression)
2027
+ return [];
2028
+ if (!ts3.isArrayLiteralExpression(expression)) {
2029
+ ctx.diagnostics.push({
2030
+ severity: "error",
2031
+ code: "dynamic-aspect-reference",
2032
+ message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
2033
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
2034
+ line: lineOf(expression),
2035
+ suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
2036
+ errorCode: "SC4010",
2037
+ docsUrl: "https://supacloud.dev/errors/SC4010"
2038
+ });
2039
+ return [];
2040
+ }
2041
+ const refs = [];
2042
+ for (const element of expression.elements) {
2043
+ if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
2044
+ ctx.diagnostics.push({
2045
+ severity: "error",
2046
+ code: "dynamic-aspect-reference",
2047
+ message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
2048
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
2049
+ line: lineOf(element),
2050
+ suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
2051
+ errorCode: "SC4010",
2052
+ docsUrl: "https://supacloud.dev/errors/SC4010"
2053
+ });
2054
+ continue;
2055
+ }
2056
+ const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
2057
+ if (!declaration) {
2058
+ ctx.diagnostics.push({
2059
+ severity: "error",
2060
+ code: "invalid-aspect-reference",
2061
+ message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
2062
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
2063
+ line: lineOf(element),
2064
+ suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
2065
+ errorCode: "SC4011",
2066
+ docsUrl: "https://supacloud.dev/errors/SC4011"
2067
+ });
2068
+ continue;
2069
+ }
2070
+ const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
2071
+ if (!name)
2072
+ continue;
2073
+ const declaredFile = declaration.getSourceFile().fileName;
2074
+ const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2075
+ refs.push({
2076
+ name,
2077
+ expression: element.text,
2078
+ importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
2079
+ importModule: projectLocal ? undefined : importModuleOf(element, ctx)
2080
+ });
2081
+ }
2082
+ return refs;
1177
2083
  }
1178
2084
  function booleanProp(obj, name) {
1179
2085
  const expr = getProp(obj, name);
1180
2086
  if (!expr)
1181
2087
  return;
1182
- if (expr.getKind() === SyntaxKind.TrueKeyword)
2088
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
1183
2089
  return true;
1184
- if (expr.getKind() === SyntaxKind.FalseKeyword)
2090
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
1185
2091
  return false;
1186
2092
  return;
1187
2093
  }
1188
2094
  function parseScopeProp(obj) {
1189
2095
  const scope = stringLiteralProp(obj, "scope");
1190
- return scope && SCOPES.includes(scope) ? scope : undefined;
2096
+ return scope && isScope(scope) ? scope : undefined;
1191
2097
  }
1192
2098
  function parseBindingOptions(args, defaultName) {
1193
2099
  let name = defaultName;
@@ -1195,16 +2101,16 @@ function parseBindingOptions(args, defaultName) {
1195
2101
  let defaultValue;
1196
2102
  const first = args[0];
1197
2103
  const second = args[1];
1198
- if (first && Node.isStringLiteral(first)) {
1199
- name = first.getLiteralText();
1200
- } else if (first && Node.isObjectLiteralExpression(first)) {
2104
+ if (first && ts3.isStringLiteral(first)) {
2105
+ name = first.text;
2106
+ } else if (first && ts3.isObjectLiteralExpression(first)) {
1201
2107
  const nameProp = getProp(first, "name");
1202
- if (nameProp && Node.isStringLiteral(nameProp)) {
1203
- name = nameProp.getLiteralText();
2108
+ if (nameProp && ts3.isStringLiteral(nameProp)) {
2109
+ name = nameProp.text;
1204
2110
  }
1205
2111
  const trProp = getProp(first, "transform");
1206
- if (trProp && Node.isStringLiteral(trProp)) {
1207
- const val = trProp.getLiteralText();
2112
+ if (trProp && ts3.isStringLiteral(trProp)) {
2113
+ const val = trProp.text;
1208
2114
  if (val === "number" || val === "boolean" || val === "string") {
1209
2115
  transform = val;
1210
2116
  }
@@ -1214,10 +2120,10 @@ function parseBindingOptions(args, defaultName) {
1214
2120
  defaultValue = parseLiteralValue(defProp);
1215
2121
  }
1216
2122
  }
1217
- if (second && Node.isObjectLiteralExpression(second)) {
2123
+ if (second && ts3.isObjectLiteralExpression(second)) {
1218
2124
  const trProp = getProp(second, "transform");
1219
- if (trProp && Node.isStringLiteral(trProp)) {
1220
- const val = trProp.getLiteralText();
2125
+ if (trProp && ts3.isStringLiteral(trProp)) {
2126
+ const val = trProp.text;
1221
2127
  if (val === "number" || val === "boolean" || val === "string") {
1222
2128
  transform = val;
1223
2129
  }
@@ -1230,28 +2136,28 @@ function parseBindingOptions(args, defaultName) {
1230
2136
  return { name, transform, default: defaultValue };
1231
2137
  }
1232
2138
  function parseLiteralValue(node) {
1233
- if (Node.isStringLiteral(node))
1234
- return node.getLiteralText();
1235
- if (Node.isNumericLiteral(node))
1236
- return node.getLiteralValue();
1237
- if (node.getKindName() === "TrueKeyword")
2139
+ if (ts3.isStringLiteral(node))
2140
+ return node.text;
2141
+ if (ts3.isNumericLiteral(node))
2142
+ return Number(node.text);
2143
+ if (node.kind === ts3.SyntaxKind.TrueKeyword)
1238
2144
  return true;
1239
- if (node.getKindName() === "FalseKeyword")
2145
+ if (node.kind === ts3.SyntaxKind.FalseKeyword)
1240
2146
  return false;
1241
- if (Node.isArrayLiteralExpression(node)) {
1242
- return node.getElements().map(parseLiteralValue);
2147
+ if (ts3.isArrayLiteralExpression(node)) {
2148
+ return node.elements.map(parseLiteralValue);
1243
2149
  }
1244
- if (Node.isObjectLiteralExpression(node)) {
2150
+ if (ts3.isObjectLiteralExpression(node)) {
1245
2151
  return parseObjectLiteralValues(node);
1246
2152
  }
1247
2153
  return;
1248
2154
  }
1249
2155
  function parseObjectLiteralValues(obj) {
1250
2156
  const result = {};
1251
- for (const prop of obj.getProperties()) {
1252
- if (Node.isPropertyAssignment(prop)) {
1253
- const name = prop.getName();
1254
- const init = prop.getInitializer();
2157
+ for (const prop of obj.properties) {
2158
+ if (ts3.isPropertyAssignment(prop)) {
2159
+ const name = propertyName(prop.name);
2160
+ const init = prop.initializer;
1255
2161
  if (init) {
1256
2162
  result[name] = parseLiteralValue(init);
1257
2163
  }
@@ -1269,63 +2175,9 @@ function warn(ctx, code, message, file, line) {
1269
2175
  ctx.diagnostics.push({ severity: "warn", code, message, file, line });
1270
2176
  }
1271
2177
  // src/generate.ts
1272
- import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
2178
+ import { createHash as createHash4 } from "node:crypto";
2179
+ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
1273
2180
  import { join as join2 } from "node:path";
1274
-
1275
- // src/util.ts
1276
- function camelName(token) {
1277
- const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
1278
- if (isConstantCase) {
1279
- return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1280
- }
1281
- return token.charAt(0).toLowerCase() + token.slice(1);
1282
- }
1283
- function relativeImportPath(fromDir, toFile) {
1284
- const fromParts = fromDir.split("/").filter(Boolean);
1285
- const toParts = toFile.split("/").filter(Boolean);
1286
- let common = 0;
1287
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
1288
- common += 1;
1289
- }
1290
- const ups = fromParts.length - common;
1291
- const downs = toParts.slice(common);
1292
- const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
1293
- const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
1294
- const joined = segments.join("/");
1295
- return joined.startsWith("..") ? joined : `./${joined}`;
1296
- }
1297
- var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
1298
- var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
1299
- function isRequestContextToken(token, tokenNames) {
1300
- return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
1301
- }
1302
- function isJobContextToken(token, tokenNames) {
1303
- return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
1304
- }
1305
- function joinRoutePaths(prefix, path) {
1306
- const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
1307
- const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
1308
- return normalized;
1309
- }
1310
- function findClosestMatch(target, candidates) {
1311
- if (candidates.length === 0)
1312
- return;
1313
- if (candidates.length === 1)
1314
- return candidates[0];
1315
- const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1316
- const targetNorm = norm(target);
1317
- for (const c of candidates) {
1318
- if (norm(c) === targetNorm)
1319
- return c;
1320
- }
1321
- for (const c of candidates) {
1322
- if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
1323
- return c;
1324
- }
1325
- return candidates[0];
1326
- }
1327
-
1328
- // src/generate.ts
1329
2181
  var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
1330
2182
  var INTERFACES = `export interface CompiledRoute {
1331
2183
  method: string;
@@ -1348,6 +2200,7 @@ var INTERFACES = `export interface CompiledRoute {
1348
2200
  queryDefaults?: Record<string, unknown>;
1349
2201
  title?: string;
1350
2202
  data?: Record<string, unknown>;
2203
+ aspects?: CompiledAspect[];
1351
2204
  invoker?: (
1352
2205
  controller: unknown,
1353
2206
  request: {
@@ -1368,8 +2221,33 @@ export interface CompiledCommand {
1368
2221
  audit?: string;
1369
2222
  idempotency: "required" | "none";
1370
2223
  standalone?: boolean;
2224
+ aspects?: CompiledAspect[];
2225
+ }
2226
+
2227
+ export interface CompiledJob {
2228
+ className: string;
2229
+ name: string;
2230
+ serviceKey: string;
2231
+ scope: "application" | "request" | "job";
2232
+ aspects?: CompiledAspect[];
2233
+ }
2234
+
2235
+ export interface CompiledAspectContext {
2236
+ kind: "route" | "command" | "job";
2237
+ name: string;
2238
+ input: unknown;
2239
+ request?: Request;
2240
+ requestContext?: unknown;
2241
+ scope?: Record<string, unknown>;
2242
+ services?: Record<string, unknown>;
2243
+ metadata?: unknown;
1371
2244
  }
1372
2245
 
2246
+ export type CompiledAspect = (
2247
+ context: CompiledAspectContext,
2248
+ next: () => unknown | Promise<unknown>,
2249
+ ) => unknown | Promise<unknown>;
2250
+
1373
2251
  export interface CompiledController {
1374
2252
  path: string;
1375
2253
  serviceKey: string;
@@ -1387,14 +2265,63 @@ export interface CompiledModule {
1387
2265
  services: Record<string, unknown>,
1388
2266
  ctx: unknown,
1389
2267
  imported?: Record<string, Record<string, unknown>>,
1390
- ): Record<string, unknown>;
2268
+ ): Promise<Record<string, unknown>>;
2269
+ destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
1391
2270
  createJobScope?(
1392
2271
  services: Record<string, unknown>,
1393
2272
  ctx: unknown,
1394
2273
  imported?: Record<string, Record<string, unknown>>,
1395
- ): Record<string, unknown>;
2274
+ ): Promise<Record<string, unknown>>;
2275
+ destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
1396
2276
  controllers: CompiledController[];
1397
2277
  commands: CompiledCommand[];
2278
+ jobs: CompiledJob[];
2279
+ aspects?: CompiledAspect[];
2280
+ }`;
2281
+ var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
2282
+ return typeof value === "object" && value !== null;
2283
+ }
2284
+
2285
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2286
+ return typeof value === "function";
2287
+ }
2288
+
2289
+ function resolveFactoryValue(value: unknown): unknown {
2290
+ if (!isRecord(value) || !isFunction(value.factory)) return undefined;
2291
+ return value.factory();
2292
+ }
2293
+
2294
+ const scopeDestructions = new WeakMap<object, Promise<void>>();
2295
+
2296
+ function destroyScopeInstances(
2297
+ scope: Record<string, unknown>,
2298
+ plan: readonly { key: string; index?: number }[],
2299
+ ): Promise<void> {
2300
+ const pending = scopeDestructions.get(scope);
2301
+ if (pending) return pending;
2302
+ const destruction = Promise.resolve().then(async () => {
2303
+ const errors: unknown[] = [];
2304
+ const seen = new Set<unknown>();
2305
+ for (const entry of [...plan].reverse()) {
2306
+ const value = scope[entry.key];
2307
+ const instance = entry.index === undefined ? value
2308
+ : Array.isArray(value) ? value[entry.index] : undefined;
2309
+ if (seen.has(instance)) continue;
2310
+ seen.add(instance);
2311
+ try {
2312
+ if (isRecord(instance) && isFunction(instance.onDestroy)) {
2313
+ await instance.onDestroy();
2314
+ } else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
2315
+ await instance.ngOnDestroy();
2316
+ }
2317
+ } catch (error) {
2318
+ errors.push(error);
2319
+ }
2320
+ }
2321
+ if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
2322
+ });
2323
+ scopeDestructions.set(scope, destruction);
2324
+ return destruction;
1398
2325
  }`;
1399
2326
  function renderApplication(graph, options) {
1400
2327
  let modules = topoSortModules(graph.modules);
@@ -1412,6 +2339,8 @@ function renderApplication(graph, options) {
1412
2339
  referencedTokens.add(d);
1413
2340
  for (const d of ctrl.skipSelfDeps ?? [])
1414
2341
  referencedTokens.add(d);
2342
+ for (const d of ctrl.hostDeps ?? [])
2343
+ referencedTokens.add(d);
1415
2344
  }
1416
2345
  for (const p of mod.providers) {
1417
2346
  for (const d of p.deps ?? [])
@@ -1422,6 +2351,8 @@ function renderApplication(graph, options) {
1422
2351
  referencedTokens.add(d);
1423
2352
  for (const d of p.skipSelfDeps ?? [])
1424
2353
  referencedTokens.add(d);
2354
+ for (const d of p.hostDeps ?? [])
2355
+ referencedTokens.add(d);
1425
2356
  if (p.useExisting)
1426
2357
  referencedTokens.add(p.useExisting);
1427
2358
  }
@@ -1446,6 +2377,8 @@ function renderApplication(graph, options) {
1446
2377
  ...imports.size > 0 ? [""] : [],
1447
2378
  INTERFACES,
1448
2379
  "",
2380
+ TYPE_GUARDS,
2381
+ "",
1449
2382
  "export function createCompiledModules(): CompiledModule[] {",
1450
2383
  " return [",
1451
2384
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
@@ -1453,29 +2386,33 @@ function renderApplication(graph, options) {
1453
2386
  "}",
1454
2387
  "",
1455
2388
  "export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
1456
- ' const initializers = (services.appInitializer ?? (services as any)["supacloud.app-initializer"]) as unknown;',
1457
- " if (Array.isArray(initializers)) {",
1458
- " for (const init of initializers) {",
1459
- ' if (typeof init === "function") await init();',
2389
+ ' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
2390
+ " for (const group of initializers) {",
2391
+ " if (Array.isArray(group)) {",
2392
+ " for (const init of group) {",
2393
+ " if (isFunction(init)) await init();",
2394
+ " }",
2395
+ " } else if (isFunction(group)) {",
2396
+ " await group();",
1460
2397
  " }",
1461
- ' } else if (typeof initializers === "function") {',
1462
- " await (initializers as () => unknown)();",
1463
2398
  " }",
1464
2399
  "}",
1465
2400
  "",
1466
2401
  "export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
1467
- ' const destroyRef = (services.destroyRef ?? (services as any)["supacloud.destroy-ref"]) as { destroy?: () => Promise<void>; _teardowns?: Array<() => void | Promise<void>> } | undefined;',
1468
- ' if (destroyRef && typeof destroyRef.destroy === "function") {',
2402
+ ' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
2403
+ " if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
1469
2404
  " await destroyRef.destroy();",
1470
- " } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
2405
+ " } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
1471
2406
  " for (const teardown of [...destroyRef._teardowns].reverse()) {",
1472
- ' if (typeof teardown === "function") await teardown();',
2407
+ " if (isFunction(teardown)) await teardown();",
1473
2408
  " }",
1474
2409
  " }",
1475
2410
  " const instances = Object.values(services);",
1476
2411
  " for (const inst of instances.reverse()) {",
1477
- ' if (inst && typeof (inst as any).onDestroy === "function") {',
1478
- " await (inst as any).onDestroy();",
2412
+ " if (isRecord(inst) && isFunction(inst.onDestroy)) {",
2413
+ " await inst.onDestroy();",
2414
+ " } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
2415
+ " await inst.ngOnDestroy();",
1479
2416
  " }",
1480
2417
  " }",
1481
2418
  "}",
@@ -1504,21 +2441,39 @@ async function generateApplication(graph, options) {
1504
2441
  await mkdir(options.outDir, { recursive: true });
1505
2442
  const applicationPath = join2(options.outDir, "application.ts");
1506
2443
  const manifestPath = join2(options.outDir, "app.manifest.json");
1507
- await writeFileAtomic(applicationPath, rendered.applicationCode);
1508
- await writeFileAtomic(manifestPath, rendered.manifestJson);
1509
- const written = [applicationPath, manifestPath];
2444
+ const written = [];
2445
+ if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
2446
+ written.push(applicationPath);
2447
+ }
2448
+ if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
2449
+ written.push(manifestPath);
2450
+ }
1510
2451
  if (rendered.clientCode) {
1511
2452
  const clientPath = join2(options.outDir, "client.ts");
1512
- await writeFileAtomic(clientPath, rendered.clientCode);
1513
- written.push(clientPath);
2453
+ if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
2454
+ written.push(clientPath);
2455
+ }
1514
2456
  }
1515
2457
  if (rendered.permissionsCode) {
1516
2458
  const permissionsPath = join2(options.outDir, "permissions.ts");
1517
- await writeFileAtomic(permissionsPath, rendered.permissionsCode);
1518
- written.push(permissionsPath);
2459
+ if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
2460
+ written.push(permissionsPath);
2461
+ }
1519
2462
  }
1520
2463
  return written;
1521
2464
  }
2465
+ async function writeFileIfChanged(path, content, hashes) {
2466
+ const hash = createHash4("sha1").update(content).digest("hex");
2467
+ if (hashes?.get(path) === hash) {
2468
+ try {
2469
+ await access(path);
2470
+ return false;
2471
+ } catch {}
2472
+ }
2473
+ await writeFileAtomic(path, content);
2474
+ hashes?.set(path, hash);
2475
+ return true;
2476
+ }
1522
2477
  async function writeFileAtomic(path, content) {
1523
2478
  const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1524
2479
  try {
@@ -1569,11 +2524,13 @@ class ImportManager {
1569
2524
  get size() {
1570
2525
  return this.entries.size;
1571
2526
  }
1572
- add(exported, importPath) {
1573
- if (!importPath)
2527
+ add(exported, importPath, importModule) {
2528
+ const path = importModule ?? importPath;
2529
+ const packageImport = importModule !== undefined;
2530
+ if (!path)
1574
2531
  return exported;
1575
2532
  for (const [local2, entry] of this.entries) {
1576
- if (entry.path === importPath && entry.exported === exported)
2533
+ if (entry.path === path && entry.exported === exported && entry.package === packageImport)
1577
2534
  return local2;
1578
2535
  }
1579
2536
  let local = exported;
@@ -1582,18 +2539,18 @@ class ImportManager {
1582
2539
  local = `${exported}${counter}`;
1583
2540
  counter += 1;
1584
2541
  }
1585
- this.entries.set(local, { path: importPath, exported });
2542
+ this.entries.set(local, { path, exported, package: packageImport });
1586
2543
  return local;
1587
2544
  }
1588
2545
  render(rootDir, outDir) {
1589
2546
  const byPath = new Map;
1590
2547
  for (const [local, entry] of this.entries) {
1591
- const list = byPath.get(entry.path) ?? [];
2548
+ const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
2549
+ const list = byPath.get(spec) ?? [];
1592
2550
  list.push({ exported: entry.exported, local });
1593
- byPath.set(entry.path, list);
2551
+ byPath.set(spec, list);
1594
2552
  }
1595
- return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
1596
- const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
2553
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
1597
2554
  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(", ");
1598
2555
  return `import { ${names} } from "${spec}";`;
1599
2556
  });
@@ -1615,14 +2572,19 @@ class ModuleGenerator {
1615
2572
  this.module = module;
1616
2573
  this.imports = imports;
1617
2574
  this.pascal = pascalName(module.name);
2575
+ if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
2576
+ imports.add("runInInjectionContext", undefined, "@supacloud/app");
2577
+ }
1618
2578
  }
1619
2579
  renderFactories() {
1620
2580
  const sections = [this.renderServicesFactory()];
1621
2581
  if (this.hasFactoryContent("request")) {
1622
2582
  sections.push(this.renderScopeFactory("request"));
2583
+ sections.push(this.renderScopeDestroyer("request"));
1623
2584
  }
1624
2585
  if (this.hasFactoryContent("job")) {
1625
2586
  sections.push(this.renderScopeFactory("job"));
2587
+ sections.push(this.renderScopeDestroyer("job"));
1626
2588
  }
1627
2589
  return sections;
1628
2590
  }
@@ -1634,12 +2596,18 @@ class ModuleGenerator {
1634
2596
  ];
1635
2597
  if (this.hasFactoryContent("request")) {
1636
2598
  lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
2599
+ lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
1637
2600
  }
1638
2601
  if (this.hasFactoryContent("job")) {
1639
2602
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
2603
+ lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
1640
2604
  }
1641
2605
  lines.push(` controllers: ${this.renderControllers()},`);
1642
- lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
2606
+ lines.push(` commands: ${this.renderCommands()},`);
2607
+ lines.push(` jobs: ${this.renderJobs()},`);
2608
+ if (this.module.aspects && this.module.aspects.length > 0) {
2609
+ lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
2610
+ }
1643
2611
  lines.push(`}`);
1644
2612
  return lines.join(`
1645
2613
  `);
@@ -1702,6 +2670,9 @@ class ModuleGenerator {
1702
2670
  if (route.data && Object.keys(route.data).length > 0) {
1703
2671
  fields.push(`data: ${JSON.stringify(route.data)}`);
1704
2672
  }
2673
+ if (route.aspects && route.aspects.length > 0) {
2674
+ fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2675
+ }
1705
2676
  const invokerArgs = (route.handlerParams ?? []).map((hp) => {
1706
2677
  if (hp.kind === "param") {
1707
2678
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
@@ -1740,7 +2711,7 @@ class ModuleGenerator {
1740
2711
  return "undefined";
1741
2712
  });
1742
2713
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
1743
- fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
2714
+ 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}]); }`);
1744
2715
  return `{ ${fields.join(", ")} }`;
1745
2716
  });
1746
2717
  return [
@@ -1757,6 +2728,32 @@ class ModuleGenerator {
1757
2728
  ${indent(item, 2)}`).join(",")}
1758
2729
  ]`;
1759
2730
  }
2731
+ renderCommands() {
2732
+ if (this.module.commands.length === 0)
2733
+ return "[]";
2734
+ return `[${this.module.commands.map((command) => {
2735
+ const fields = [
2736
+ `className: ${JSON.stringify(command.className)}`,
2737
+ `name: ${JSON.stringify(command.name)}`,
2738
+ `permission: ${JSON.stringify(command.permission ?? "")}`,
2739
+ `transaction: ${JSON.stringify(command.transaction)}`,
2740
+ ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2741
+ `idempotency: ${JSON.stringify(command.idempotency)}`,
2742
+ ...command.standalone ? ["standalone: true"] : [],
2743
+ ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2744
+ ];
2745
+ return `{ ${fields.join(", ")} }`;
2746
+ }).join(", ")}]`;
2747
+ }
2748
+ renderJobs() {
2749
+ const jobs = this.module.jobs ?? [];
2750
+ if (jobs.length === 0)
2751
+ return "[]";
2752
+ return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(job.serviceKey)}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
2753
+ }
2754
+ renderAspects(aspects) {
2755
+ return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
2756
+ }
1760
2757
  renderServicesFactory() {
1761
2758
  return [
1762
2759
  `function create${this.pascal}Services(`,
@@ -1771,17 +2768,51 @@ ${indent(item, 2)}`).join(",")}
1771
2768
  renderScopeFactory(kind) {
1772
2769
  const suffix = kind === "request" ? "RequestScope" : "JobScope";
1773
2770
  return [
1774
- `function create${this.pascal}${suffix}(`,
2771
+ `async function create${this.pascal}${suffix}(`,
1775
2772
  ` services: Record<string, unknown>,`,
1776
2773
  ` ctx: unknown,`,
1777
2774
  ` imported: Record<string, Record<string, unknown>> = {},`,
1778
- `): Record<string, unknown> {`,
1779
- indent(this.renderFactoryBody(kind), 2),
2775
+ `): Promise<Record<string, unknown>> {`,
2776
+ ` const scope: Record<string, unknown> = {};`,
2777
+ ` try {`,
2778
+ indent(this.renderFactoryBody(kind, true), 4),
2779
+ ` } catch (error) {`,
2780
+ ` try {`,
2781
+ ` await destroy${this.pascal}${suffix}(scope);`,
2782
+ ` } catch (cleanupError) {`,
2783
+ ` console.error("supacloud: ${kind} scope rollback failed for ${this.module.name}", cleanupError);`,
2784
+ ` }`,
2785
+ ` throw error;`,
2786
+ ` }`,
2787
+ `}`
2788
+ ].join(`
2789
+ `);
2790
+ }
2791
+ renderScopeDestroyer(kind) {
2792
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
2793
+ const plan = [];
2794
+ const multiIndices = new Map;
2795
+ for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
2796
+ const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
2797
+ if (index !== undefined)
2798
+ multiIndices.set(provider.token, index + 1);
2799
+ if (provider.kind !== "existing" && provider.hasOnDestroy) {
2800
+ plan.push({ key: camelName(provider.token), index });
2801
+ }
2802
+ }
2803
+ for (const controller of this.module.controllers) {
2804
+ if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
2805
+ plan.push({ key: camelName(controller.className) });
2806
+ }
2807
+ }
2808
+ return [
2809
+ `async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
2810
+ ` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
1780
2811
  `}`
1781
2812
  ].join(`
1782
2813
  `);
1783
2814
  }
1784
- renderFactoryBody(kind) {
2815
+ renderFactoryBody(kind, scoped = false) {
1785
2816
  const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
1786
2817
  const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
1787
2818
  const lines = [];
@@ -1792,6 +2823,9 @@ ${indent(item, 2)}`).join(",")}
1792
2823
  const emitted = this.emitProvider(provider, kind, true);
1793
2824
  if (emitted.constLine)
1794
2825
  lines.push(emitted.constLine);
2826
+ if (scoped) {
2827
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = [...(Array.isArray(scope[${JSON.stringify(emitted.key)}]) ? scope[${JSON.stringify(emitted.key)}] : []), ${emitted.expr}];`);
2828
+ }
1795
2829
  const list = multiGroups.get(emitted.key) ?? [];
1796
2830
  list.push(emitted.expr);
1797
2831
  multiGroups.set(emitted.key, list);
@@ -1799,6 +2833,9 @@ ${indent(item, 2)}`).join(",")}
1799
2833
  const emitted = this.emitProvider(provider, kind, false);
1800
2834
  if (emitted.constLine)
1801
2835
  lines.push(emitted.constLine);
2836
+ if (scoped) {
2837
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
2838
+ }
1802
2839
  returns.set(emitted.key, emitted.expr);
1803
2840
  }
1804
2841
  }
@@ -1808,8 +2845,16 @@ ${indent(item, 2)}`).join(",")}
1808
2845
  for (const controller of controllers) {
1809
2846
  const emitted = this.emitController(controller, kind);
1810
2847
  lines.push(emitted.constLine);
2848
+ if (scoped) {
2849
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
2850
+ }
1811
2851
  returns.set(emitted.key, emitted.expr);
1812
2852
  }
2853
+ if (scoped) {
2854
+ lines.push(`return scope;`);
2855
+ return lines.join(`
2856
+ `);
2857
+ }
1813
2858
  const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
1814
2859
  lines.push(`return { ${entries.join(", ")} };`);
1815
2860
  return lines.join(`
@@ -1819,39 +2864,76 @@ ${indent(item, 2)}`).join(",")}
1819
2864
  const key = camelName(provider.token);
1820
2865
  switch (provider.kind) {
1821
2866
  case "class": {
1822
- const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
1823
- const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
2867
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
2868
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1824
2869
  const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
1825
- return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
2870
+ return {
2871
+ constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
2872
+ key,
2873
+ expr: local
2874
+ };
1826
2875
  }
1827
2876
  case "value": {
1828
- const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
2877
+ const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
1829
2878
  const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1830
2879
  return { constLine: `const ${local} = ${expr};`, key, expr: local };
1831
2880
  }
1832
2881
  case "factory": {
1833
2882
  if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
1834
- const tokenIdent = this.imports.add(provider.token, provider.importPath);
2883
+ const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
1835
2884
  const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1836
- const constLine = `const ${local2} = typeof ${tokenIdent} === "object" && ${tokenIdent} && "factory" in ${tokenIdent} && typeof (${tokenIdent} as any).factory === "function" ? (${tokenIdent} as any).factory() : undefined;`;
2885
+ const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
1837
2886
  return { constLine, key, expr: local2 };
1838
2887
  }
1839
- const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
1840
- const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
2888
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2889
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1841
2890
  const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
1842
2891
  return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
1843
2892
  }
1844
2893
  case "existing": {
1845
- return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
2894
+ return {
2895
+ key,
2896
+ expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
2897
+ };
1846
2898
  }
1847
2899
  }
1848
2900
  }
1849
2901
  emitController(controller, kind) {
1850
2902
  const className = this.imports.add(controller.className, controller.importPath);
1851
- const args = controller.deps.map((dep) => this.depExpr(dep, kind, controller.optionalDeps?.includes(dep))).join(", ");
2903
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
1852
2904
  const key = camelName(controller.className);
1853
2905
  const local = this.localVar(controller.className, kind);
1854
- return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
2906
+ return {
2907
+ constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
2908
+ key,
2909
+ expr: local
2910
+ };
2911
+ }
2912
+ instantiate(className, args, kind, functionalInjects) {
2913
+ if (!functionalInjects || functionalInjects.length === 0) {
2914
+ return `new ${className}(${args})`;
2915
+ }
2916
+ const clauses = functionalInjects.map((entry) => {
2917
+ const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
2918
+ const value = this.depExpr(entry.token, kind, {
2919
+ optional: entry.optional,
2920
+ self: entry.self,
2921
+ skipSelf: entry.skipSelf,
2922
+ host: entry.host
2923
+ });
2924
+ return `if (token === ${token}) return ${value} as T;`;
2925
+ });
2926
+ const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
2927
+ const injector = [
2928
+ `{`,
2929
+ `get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
2930
+ ...clauses,
2931
+ missing,
2932
+ `},`,
2933
+ `}`
2934
+ ].join(`
2935
+ `);
2936
+ return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
1855
2937
  }
1856
2938
  localVar(token, kind) {
1857
2939
  const locals = this.locals[kind];
@@ -1868,23 +2950,34 @@ ${indent(item, 2)}`).join(",")}
1868
2950
  locals.set(token, local);
1869
2951
  return local;
1870
2952
  }
1871
- depExpr(token, kind, isOptional = false) {
2953
+ depOptions(node, token) {
2954
+ return {
2955
+ optional: node.optionalDeps?.includes(token) ?? false,
2956
+ self: node.selfDeps?.includes(token) ?? false,
2957
+ skipSelf: node.skipSelfDeps?.includes(token) ?? false,
2958
+ host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
2959
+ };
2960
+ }
2961
+ depExpr(token, kind, options = {}) {
2962
+ const isOptional = options.optional ?? false;
2963
+ const isSelf = options.self ?? false;
2964
+ const isSkipSelf = options.skipSelf ?? false;
1872
2965
  if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
1873
2966
  return "ctx";
1874
2967
  if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
1875
2968
  return "ctx";
1876
2969
  const own = this.module.providers.find((p) => p.token === token);
1877
- if (own) {
2970
+ const ownIsLocal = own && factoryOfScope(own.scope) === kind;
2971
+ if (own && ownIsLocal && !isSkipSelf) {
1878
2972
  if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
1879
2973
  return this.locals[kind].get(token) ?? camelName(token);
1880
2974
  }
1881
- if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
1882
- return this.depExpr(own.useExisting ?? token, kind, isOptional);
2975
+ if (own.kind === "existing") {
2976
+ return this.depExpr(own.useExisting ?? token, kind, options);
1883
2977
  }
1884
- if (kind === "services") {
1885
- return `services.${camelName(token)}`;
1886
- }
1887
- return `services.${camelName(token)}`;
2978
+ }
2979
+ if (isSelf) {
2980
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1888
2981
  }
1889
2982
  for (const importName of this.module.imports) {
1890
2983
  const imported = this.graph.modules.find((m) => m.name === importName);
@@ -1903,6 +2996,8 @@ ${indent(item, 2)}`).join(",")}
1903
2996
  if (isOptional && !this.graph.externalTokens.includes(token)) {
1904
2997
  return "undefined";
1905
2998
  }
2999
+ if (isSelf)
3000
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1906
3001
  if (kind === "services")
1907
3002
  return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
1908
3003
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
@@ -2360,12 +3455,17 @@ var COMPILER_DIAGNOSTIC_CODES = {
2360
3455
  "conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
2361
3456
  "missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
2362
3457
  "missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
3458
+ "provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
3459
+ "unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
2363
3460
  "command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
2364
3461
  "duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
2365
3462
  "route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
2366
3463
  "command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
2367
3464
  "route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
2368
3465
  "command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
3466
+ "invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
3467
+ "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3468
+ "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
2369
3469
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
2370
3470
  };
2371
3471
  function validateGraph(graph, options = false) {
@@ -2399,10 +3499,14 @@ function validateGraph(graph, options = false) {
2399
3499
  }
2400
3500
  }
2401
3501
  }
2402
- function resolveDep(module, token) {
2403
- const own = module.providers.find((p) => p.token === token);
2404
- if (own)
2405
- return { module, provider: own };
3502
+ function resolveDep(module, token, flags = {}) {
3503
+ if (!flags.skipSelf) {
3504
+ const own = module.providers.find((p) => p.token === token);
3505
+ if (own)
3506
+ return { module, provider: own };
3507
+ }
3508
+ if (flags.self)
3509
+ return;
2406
3510
  for (const importName of module.imports) {
2407
3511
  const imported = graph.modules.find((m) => m.name === importName);
2408
3512
  if (!imported || !imported.exports.includes(token))
@@ -2600,7 +3704,7 @@ function validateGraph(graph, options = false) {
2600
3704
  }
2601
3705
  if (controller.selfDeps && controller.selfDeps.length > 0) {
2602
3706
  for (const dep of controller.selfDeps) {
2603
- const own = module.providers.find((p) => p.token === dep);
3707
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2604
3708
  if (!own) {
2605
3709
  error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2606
3710
  }
@@ -2608,7 +3712,7 @@ function validateGraph(graph, options = false) {
2608
3712
  }
2609
3713
  if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
2610
3714
  for (const dep of controller.skipSelfDeps) {
2611
- const own = module.providers.find((p) => p.token === dep);
3715
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2612
3716
  if (own) {
2613
3717
  error("skip-self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, controller.file, undefined, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2614
3718
  }
@@ -2683,7 +3787,7 @@ function validateGraph(graph, options = false) {
2683
3787
  for (const provider of module.providers) {
2684
3788
  if (provider.selfDeps && provider.selfDeps.length > 0) {
2685
3789
  for (const dep of provider.selfDeps) {
2686
- const own = module.providers.find((p) => p.token === dep);
3790
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2687
3791
  if (!own) {
2688
3792
  error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2689
3793
  }
@@ -2691,7 +3795,7 @@ function validateGraph(graph, options = false) {
2691
3795
  }
2692
3796
  if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
2693
3797
  for (const dep of provider.skipSelfDeps) {
2694
- const own = module.providers.find((p) => p.token === dep);
3798
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2695
3799
  if (own) {
2696
3800
  error("skip-self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, provider.file, provider.line, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2697
3801
  }
@@ -2699,7 +3803,10 @@ function validateGraph(graph, options = false) {
2699
3803
  }
2700
3804
  for (const dep of provider.deps) {
2701
3805
  const isOptional = provider.optionalDeps?.includes(dep);
2702
- const resolved = resolveDep(module, dep);
3806
+ const resolved = resolveDep(module, dep, {
3807
+ self: provider.selfDeps?.includes(dep),
3808
+ skipSelf: provider.skipSelfDeps?.includes(dep)
3809
+ });
2703
3810
  if (!resolved) {
2704
3811
  if (isOptional) {
2705
3812
  continue;
@@ -2707,6 +3814,8 @@ function validateGraph(graph, options = false) {
2707
3814
  if (!graph.externalTokens.includes(dep)) {
2708
3815
  if (globalProviders.has(dep)) {
2709
3816
  const owner = globalProviders.get(dep);
3817
+ if (!owner)
3818
+ continue;
2710
3819
  error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line, `Import module '${owner.module.name}' in '${module.name}', add '${dep}' to '${owner.module.name}' exports, or mark @Injectable({ providedIn: 'root' }).`);
2711
3820
  } else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
2712
3821
  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: () => ... }).`);
@@ -2767,7 +3876,8 @@ function validateGraph(graph, options = false) {
2767
3876
  }
2768
3877
  }
2769
3878
  if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
2770
- const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
3879
+ const allowedTags = rule.onlyDependOnLibsWithTags;
3880
+ const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
2771
3881
  if (!hasAllowed && targetTags.length > 0) {
2772
3882
  error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
2773
3883
  }
@@ -2789,6 +3899,8 @@ function validateGraph(graph, options = false) {
2789
3899
  referencedTokens.add(d);
2790
3900
  for (const d of ctrl.skipSelfDeps ?? [])
2791
3901
  referencedTokens.add(d);
3902
+ for (const d of ctrl.hostDeps ?? [])
3903
+ referencedTokens.add(d);
2792
3904
  }
2793
3905
  for (const p of mod.providers) {
2794
3906
  for (const d of p.deps ?? [])
@@ -2799,6 +3911,8 @@ function validateGraph(graph, options = false) {
2799
3911
  referencedTokens.add(d);
2800
3912
  for (const d of p.skipSelfDeps ?? [])
2801
3913
  referencedTokens.add(d);
3914
+ for (const d of p.hostDeps ?? [])
3915
+ referencedTokens.add(d);
2802
3916
  if (p.useExisting)
2803
3917
  referencedTokens.add(p.useExisting);
2804
3918
  }
@@ -2921,7 +4035,10 @@ function detectCycles(graph, resolveDep) {
2921
4035
  state.set(id, "visiting");
2922
4036
  stack.push(ref);
2923
4037
  for (const dep of ref.provider.deps) {
2924
- const resolved = resolveDep(ref.module, dep);
4038
+ const resolved = resolveDep(ref.module, dep, {
4039
+ self: ref.provider.selfDeps?.includes(dep),
4040
+ skipSelf: ref.provider.skipSelfDeps?.includes(dep)
4041
+ });
2925
4042
  if (resolved)
2926
4043
  visit(resolved);
2927
4044
  }
@@ -3034,6 +4151,8 @@ function detectOrphanModules(graph) {
3034
4151
  }
3035
4152
  while (queue.length > 0) {
3036
4153
  const current = queue.shift();
4154
+ if (!current)
4155
+ continue;
3037
4156
  const mod = moduleMap.get(current);
3038
4157
  if (!mod)
3039
4158
  continue;
@@ -3062,10 +4181,225 @@ function detectOrphanModules(graph) {
3062
4181
  }
3063
4182
 
3064
4183
  // src/compile.ts
3065
- import { existsSync as existsSync2, readFileSync } from "node:fs";
3066
- import { join as join3 } from "node:path";
4184
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4185
+ import { join as join4 } from "node:path";
4186
+
4187
+ // src/type-safety.ts
4188
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4189
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4190
+ import * as ts4 from "@typescript/typescript6";
4191
+ var DEFAULT_EXCLUDES = [
4192
+ "**/*.test.ts",
4193
+ "**/*.spec.ts",
4194
+ "**/test/**",
4195
+ "**/tests/**",
4196
+ "**/__tests__/**",
4197
+ "**/fixtures/**",
4198
+ "**/generated/**",
4199
+ "**/dist/**",
4200
+ "**/*.d.ts"
4201
+ ];
4202
+ var DIAGNOSTIC_META = {
4203
+ "generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
4204
+ "source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
4205
+ "source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
4206
+ "source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
4207
+ "source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
4208
+ };
4209
+ function scanGeneratedArtifacts(artifacts, strict = true) {
4210
+ const diagnostics = [];
4211
+ for (const [file, content] of Object.entries(artifacts)) {
4212
+ if (content === undefined)
4213
+ continue;
4214
+ const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
4215
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4216
+ diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
4217
+ }
4218
+ }
4219
+ return diagnostics;
4220
+ }
4221
+ function scanProductionSource(options) {
4222
+ const rootDir = resolve2(options.rootDir);
4223
+ const configPath = join3(rootDir, "tsconfig.json");
4224
+ const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
4225
+ options: {
4226
+ strict: true,
4227
+ skipLibCheck: true,
4228
+ target: ts4.ScriptTarget.ES2022,
4229
+ module: ts4.ModuleKind.ESNext
4230
+ },
4231
+ errors: []
4232
+ };
4233
+ const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
4234
+ const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
4235
+ const compilerOptions = { ...projectConfig.options, noEmit: true };
4236
+ const host = ts4.createCompilerHost(compilerOptions);
4237
+ host.getCurrentDirectory = () => rootDir;
4238
+ const program = ts4.createProgram(rootNames, compilerOptions, host);
4239
+ const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
4240
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
4241
+ const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
4242
+ const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
4243
+ severity: "error",
4244
+ code: "source-config",
4245
+ message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
4246
+ `),
4247
+ file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
4248
+ line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
4249
+ errorCode: `TS${diagnostic.code}`
4250
+ }));
4251
+ const checker = program.getTypeChecker();
4252
+ for (const sourceFile of sourceFiles) {
4253
+ scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
4254
+ }
4255
+ return diagnostics;
4256
+ }
4257
+ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
4258
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4259
+ diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
4260
+ }
4261
+ for (const node of descendants(sourceFile)) {
4262
+ if (ts4.isAsExpression(node)) {
4263
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4264
+ continue;
4265
+ const assertedType = node.type.getText(sourceFile);
4266
+ if (assertedType === "const")
4267
+ continue;
4268
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4269
+ } else if (ts4.isTypeAssertionExpression(node)) {
4270
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4271
+ continue;
4272
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4273
+ } else if (ts4.isNonNullExpression(node)) {
4274
+ diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
4275
+ }
4276
+ }
4277
+ for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
4278
+ const initializer = declaration.initializer;
4279
+ if (!initializer || declaration.type)
4280
+ continue;
4281
+ const declarationType = checker.getTypeAtLocation(declaration.name);
4282
+ const initializerType = checker.getTypeAtLocation(initializer);
4283
+ for (const name of bindingNames(declaration.name)) {
4284
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4285
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
4286
+ }
4287
+ }
4288
+ if (isAnyType(declarationType))
4289
+ continue;
4290
+ if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
4291
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
4292
+ }
4293
+ 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))) {
4294
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
4295
+ }
4296
+ }
4297
+ for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
4298
+ if (parameter.type)
4299
+ continue;
4300
+ for (const name of bindingNames(parameter.name)) {
4301
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4302
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
4303
+ }
4304
+ }
4305
+ }
4306
+ }
4307
+ function readProjectConfig2(configPath) {
4308
+ const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
4309
+ if (config.error)
4310
+ return { options: {}, errors: [config.error] };
4311
+ const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
4312
+ return { options: parsed.options, errors: parsed.errors };
4313
+ }
4314
+ function isProductionSource(rootDir, sourceFile, excludes, outDir) {
4315
+ const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
4316
+ if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
4317
+ return false;
4318
+ if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
4319
+ return false;
4320
+ return !excludes.some((pattern) => globMatches(relativePath, pattern));
4321
+ }
4322
+ function isProductionSourcePath(rootDir, filePath, excludes) {
4323
+ const relativePath = normalizeRelative(rootDir, filePath);
4324
+ return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
4325
+ }
4326
+ function globMatches(value, pattern) {
4327
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
4328
+ return new RegExp(`^${escaped}$`).test(value);
4329
+ }
4330
+ function bindingNames(name) {
4331
+ if (ts4.isIdentifier(name))
4332
+ return [name];
4333
+ return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
4334
+ }
4335
+ function isLiteralExpression(node) {
4336
+ if (!node)
4337
+ return false;
4338
+ return [
4339
+ ts4.SyntaxKind.StringLiteral,
4340
+ ts4.SyntaxKind.NumericLiteral,
4341
+ ts4.SyntaxKind.TrueKeyword,
4342
+ ts4.SyntaxKind.FalseKeyword
4343
+ ].includes(node.kind);
4344
+ }
4345
+ function isLiteralSyntax(node) {
4346
+ return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
4347
+ }
4348
+ function isLiteralType(type) {
4349
+ return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
4350
+ }
4351
+ function isAnyType(type) {
4352
+ return (type.flags & ts4.TypeFlags.Any) !== 0;
4353
+ }
4354
+ function isLetDeclaration(declaration) {
4355
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4356
+ }
4357
+ function isConstDeclaration(declaration) {
4358
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
4359
+ }
4360
+ function descendants(root) {
4361
+ const result = [];
4362
+ const visit = (node) => {
4363
+ result.push(node);
4364
+ ts4.forEachChild(node, visit);
4365
+ };
4366
+ ts4.forEachChild(root, visit);
4367
+ return result;
4368
+ }
4369
+ function descendantsOfKind2(root, predicate) {
4370
+ const result = [];
4371
+ const visit = (node) => {
4372
+ if (predicate(node))
4373
+ result.push(node);
4374
+ ts4.forEachChild(node, visit);
4375
+ };
4376
+ ts4.forEachChild(root, visit);
4377
+ return result;
4378
+ }
4379
+ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
4380
+ const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
4381
+ const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
4382
+ const meta = DIAGNOSTIC_META[code];
4383
+ return {
4384
+ severity: strict ? "error" : "warn",
4385
+ code,
4386
+ message,
4387
+ file,
4388
+ line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
4389
+ errorCode: meta.errorCode,
4390
+ docsUrl: meta.docsUrl
4391
+ };
4392
+ }
4393
+ function normalizeRelative(rootDir, filePath) {
4394
+ return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4395
+ }
4396
+ function isAnyKeyword(node) {
4397
+ return node.kind === ts4.SyntaxKind.AnyKeyword;
4398
+ }
4399
+
4400
+ // src/compile.ts
3067
4401
  async function compileProject(options) {
3068
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4402
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3069
4403
  const diagnostics = [
3070
4404
  ...graph.diagnostics ?? [],
3071
4405
  ...validateGraph(graph, {
@@ -3084,14 +4418,40 @@ async function compileProject(options) {
3084
4418
  diagnostic.severity = "error";
3085
4419
  }
3086
4420
  }
3087
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
3088
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
4421
+ const typeSafety = resolveTypeSafety(options);
4422
+ const rendered = renderApplication(graph, {
3089
4423
  rootDir: options.rootDir,
3090
4424
  outDir: options.outDir,
3091
4425
  generateClient: options.generateClient,
3092
4426
  generatePermissions: options.generatePermissions,
3093
4427
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3094
- }) : [];
4428
+ });
4429
+ if (typeSafety.scanProductionSource) {
4430
+ diagnostics.push(...scanProductionSource({
4431
+ rootDir: options.rootDir,
4432
+ include: options.include,
4433
+ outDir: options.outDir,
4434
+ strict: options.strict,
4435
+ ...typeSafety
4436
+ }));
4437
+ }
4438
+ if (typeSafety.noAnyInGenerated) {
4439
+ diagnostics.push(...scanGeneratedArtifacts({
4440
+ "application.ts": rendered.applicationCode,
4441
+ "client.ts": rendered.clientCode,
4442
+ "permissions.ts": rendered.permissionsCode
4443
+ }, options.strict ?? false));
4444
+ }
4445
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
4446
+ const generatedOptions = {
4447
+ rootDir: options.rootDir,
4448
+ outDir: options.outDir,
4449
+ generateClient: options.generateClient,
4450
+ generatePermissions: options.generatePermissions,
4451
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4452
+ artifactHashes: options.cache?.generatedHashes
4453
+ };
4454
+ const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
3095
4455
  const stats = graph.cacheStats ? {
3096
4456
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
3097
4457
  changedFiles: [],
@@ -3102,7 +4462,7 @@ async function compileProject(options) {
3102
4462
  return { diagnostics, graph, written, stats };
3103
4463
  }
3104
4464
  async function checkProject(options) {
3105
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4465
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3106
4466
  const diagnostics = [
3107
4467
  ...graph.diagnostics ?? [],
3108
4468
  ...validateGraph(graph, {
@@ -3121,6 +4481,7 @@ async function checkProject(options) {
3121
4481
  diagnostic.severity = "error";
3122
4482
  }
3123
4483
  }
4484
+ const typeSafety = resolveTypeSafety(options);
3124
4485
  const rendered = renderApplication(graph, {
3125
4486
  rootDir: options.rootDir,
3126
4487
  outDir: options.outDir,
@@ -3128,6 +4489,22 @@ async function checkProject(options) {
3128
4489
  generatePermissions: options.generatePermissions,
3129
4490
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3130
4491
  });
4492
+ if (typeSafety.scanProductionSource) {
4493
+ diagnostics.push(...scanProductionSource({
4494
+ rootDir: options.rootDir,
4495
+ include: options.include,
4496
+ outDir: options.outDir,
4497
+ strict: options.strict,
4498
+ ...typeSafety
4499
+ }));
4500
+ }
4501
+ if (typeSafety.noAnyInGenerated) {
4502
+ diagnostics.push(...scanGeneratedArtifacts({
4503
+ "application.ts": rendered.applicationCode,
4504
+ "client.ts": rendered.clientCode,
4505
+ "permissions.ts": rendered.permissionsCode
4506
+ }, options.strict ?? false));
4507
+ }
3131
4508
  const expectedFiles = {
3132
4509
  "application.ts": rendered.applicationCode,
3133
4510
  "app.manifest.json": rendered.manifestJson
@@ -3140,12 +4517,12 @@ async function checkProject(options) {
3140
4517
  }
3141
4518
  const mismatches = [];
3142
4519
  for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
3143
- const diskPath = join3(options.outDir, filename);
3144
- if (!existsSync2(diskPath)) {
4520
+ const diskPath = join4(options.outDir, filename);
4521
+ if (!existsSync3(diskPath)) {
3145
4522
  mismatches.push(`${filename}: generated artifact is missing from disk`);
3146
4523
  continue;
3147
4524
  }
3148
- const diskContent = readFileSync(diskPath, "utf8");
4525
+ const diskContent = readFileSync3(diskPath, "utf8");
3149
4526
  if (diskContent !== expectedContent) {
3150
4527
  mismatches.push(`${filename}: disk artifact differs from current compiler output`);
3151
4528
  }
@@ -3157,29 +4534,40 @@ async function checkProject(options) {
3157
4534
  graph
3158
4535
  };
3159
4536
  }
4537
+ function resolveTypeSafety(options) {
4538
+ return {
4539
+ noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
4540
+ scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
4541
+ exclude: options.typeSafety?.exclude
4542
+ };
4543
+ }
3160
4544
  // src/watch.ts
3161
4545
  import { watch } from "node:fs";
3162
- import { relative as relative3, resolve as resolve2 } from "node:path";
4546
+ import { relative as relative4, resolve as resolve4 } from "node:path";
3163
4547
 
3164
4548
  // src/incremental.ts
3165
- import { createHash as createHash2 } from "node:crypto";
3166
- import { access, readdir, readFile } from "node:fs/promises";
3167
- import { relative as relative2, resolve, sep as sep2 } from "node:path";
4549
+ import { createHash as createHash5 } from "node:crypto";
4550
+ import { access as access2, readdir, readFile } from "node:fs/promises";
4551
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
3168
4552
  function createDependencyGraphCache() {
3169
4553
  return {
3170
4554
  modules: new Map,
3171
- fileHashes: new Map
4555
+ fileHashes: new Map,
4556
+ generatedHashes: new Map
3172
4557
  };
3173
4558
  }
3174
4559
  function createIncrementalCompiler() {
3175
4560
  let previousSnapshot;
3176
4561
  let previousResult;
4562
+ let previousCache;
3177
4563
  const cache = createDependencyGraphCache();
3178
4564
  return {
3179
4565
  async compile(options, changedPaths) {
3180
- const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
4566
+ const optionsKey = optionsKeyOf(options);
4567
+ const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
3181
4568
  const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
3182
- const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && changedFiles.length === 0);
4569
+ const activeCache = options.cache ?? cache;
4570
+ const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
3183
4571
  if (cacheHit && previousResult) {
3184
4572
  return {
3185
4573
  ...previousResult,
@@ -3192,22 +4580,14 @@ function createIncrementalCompiler() {
3192
4580
  }
3193
4581
  };
3194
4582
  }
3195
- if (previousResult && previousSnapshot && changedFiles.length > 0 && !await requiresGraphRebuild(options.rootDir, changedFiles)) {
3196
- const stats2 = {
3197
- cacheHit: true,
3198
- changedFiles,
3199
- affectedModules: findAffectedModules(previousResult.graph.modules, previousResult.graph.modules, changedFiles),
3200
- reusedModules: previousResult.graph.modules.map((m) => m.name),
3201
- reanalyzedModules: []
3202
- };
3203
- previousSnapshot = snapshot;
3204
- return { ...previousResult, written: [], stats: stats2 };
3205
- }
3206
- const activeCache = options.cache ?? cache;
3207
4583
  if (!activeCache.dependencyGraph && previousResult) {
3208
4584
  activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
3209
4585
  }
3210
- const result = await compileProject({ ...options, cache: activeCache });
4586
+ const result = await compileProject({
4587
+ ...options,
4588
+ cache: activeCache,
4589
+ changedPaths: changedFiles
4590
+ });
3211
4591
  const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
3212
4592
  const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
3213
4593
  const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
@@ -3223,14 +4603,18 @@ function createIncrementalCompiler() {
3223
4603
  };
3224
4604
  previousSnapshot = snapshot;
3225
4605
  previousResult = result;
4606
+ previousCache = activeCache;
3226
4607
  return { ...result, stats };
3227
4608
  },
3228
4609
  reset() {
3229
4610
  previousSnapshot = undefined;
3230
4611
  previousResult = undefined;
4612
+ previousCache = undefined;
3231
4613
  cache.modules.clear();
3232
4614
  cache.fileHashes.clear();
4615
+ cache.generatedHashes?.clear();
3233
4616
  cache.dependencyGraph = undefined;
4617
+ cache.programSession?.reset();
3234
4618
  },
3235
4619
  getCache() {
3236
4620
  return cache;
@@ -3238,18 +4622,21 @@ function createIncrementalCompiler() {
3238
4622
  };
3239
4623
  }
3240
4624
  async function updateSnapshot(previous, options, changedPaths) {
3241
- const rootDir = resolve(options.rootDir);
3242
- const outDir = resolve(options.outDir);
4625
+ const rootDir = resolve3(options.rootDir);
4626
+ const outDir = resolve3(options.outDir);
3243
4627
  const files = { ...previous.files };
3244
4628
  for (const changedPath of changedPaths) {
3245
- const absolutePath = resolve(rootDir, changedPath);
4629
+ const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4630
+ const relativeChangedPath = relative3(rootDir, absolutePath);
4631
+ if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
4632
+ continue;
3246
4633
  if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
3247
4634
  continue;
3248
- const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
4635
+ const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
3249
4636
  try {
3250
- await access(absolutePath);
4637
+ await access2(absolutePath);
3251
4638
  const content = await readFile(absolutePath);
3252
- files[relativePath] = createHash2("sha256").update(content).digest("hex");
4639
+ files[relativePath] = createHash5("sha256").update(content).digest("hex");
3253
4640
  } catch {
3254
4641
  delete files[relativePath];
3255
4642
  }
@@ -3257,20 +4644,23 @@ async function updateSnapshot(previous, options, changedPaths) {
3257
4644
  return { files, optionsKey: optionsKeyOf(options) };
3258
4645
  }
3259
4646
  async function createSnapshot(options) {
3260
- const rootDir = resolve(options.rootDir);
3261
- const outDir = resolve(options.outDir);
4647
+ const rootDir = resolve3(options.rootDir);
4648
+ const outDir = resolve3(options.outDir);
3262
4649
  const paths = await listSourceFiles(rootDir, outDir);
3263
4650
  const files = {};
3264
4651
  for (const path of paths) {
3265
4652
  const content = await readFile(path);
3266
- files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
4653
+ files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
3267
4654
  }
3268
4655
  return { files, optionsKey: optionsKeyOf(options) };
3269
4656
  }
3270
4657
  function optionsKeyOf(options) {
3271
4658
  return JSON.stringify({
4659
+ rootDir: resolve3(options.rootDir),
4660
+ outDir: resolve3(options.outDir),
3272
4661
  include: options.include,
3273
4662
  strict: options.strict,
4663
+ writeOnError: options.writeOnError,
3274
4664
  moduleBoundaryPreset: options.moduleBoundaryPreset,
3275
4665
  moduleBoundaries: options.moduleBoundaries,
3276
4666
  allowRouteCommandBindings: options.allowRouteCommandBindings,
@@ -3278,27 +4668,16 @@ function optionsKeyOf(options) {
3278
4668
  disallowControllerDirectDb: options.disallowControllerDirectDb,
3279
4669
  detectOrphanModules: options.detectOrphanModules,
3280
4670
  generateClient: options.generateClient,
3281
- generatePermissions: options.generatePermissions
4671
+ generatePermissions: options.generatePermissions,
4672
+ typeSafety: options.typeSafety,
4673
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
3282
4674
  });
3283
4675
  }
3284
- async function requiresGraphRebuild(rootDir, changedFiles) {
3285
- for (const relativePath of changedFiles) {
3286
- const path = resolve(rootDir, relativePath);
3287
- try {
3288
- const source = await readFile(path, "utf8");
3289
- if (/@(?:Module|Injectable|Inject|Controller|Command|Query)\b|new\s+InjectionToken\b|\bdefineModule\s*\(/.test(source))
3290
- return true;
3291
- } catch {
3292
- return true;
3293
- }
3294
- }
3295
- return false;
3296
- }
3297
4676
  async function listSourceFiles(rootDir, outDir) {
3298
4677
  const result = [];
3299
4678
  const visit = async (directory) => {
3300
4679
  for (const entry of await readdir(directory, { withFileTypes: true })) {
3301
- const path = resolve(directory, entry.name);
4680
+ const path = resolve3(directory, entry.name);
3302
4681
  if (entry.isDirectory()) {
3303
4682
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
3304
4683
  continue;
@@ -3356,7 +4735,9 @@ class ModuleDependencyGraph {
3356
4735
  if (!this.dependents.has(imp)) {
3357
4736
  this.dependents.set(imp, new Set);
3358
4737
  }
3359
- this.dependents.get(imp).add(modName);
4738
+ const dependents = this.dependents.get(imp);
4739
+ if (dependents)
4740
+ dependents.add(modName);
3360
4741
  }
3361
4742
  }
3362
4743
  }
@@ -3367,7 +4748,9 @@ class ModuleDependencyGraph {
3367
4748
  if (!this.fileOwners.has(normalized)) {
3368
4749
  this.fileOwners.set(normalized, new Set);
3369
4750
  }
3370
- this.fileOwners.get(normalized).add(moduleName);
4751
+ const owners = this.fileOwners.get(normalized);
4752
+ if (owners)
4753
+ owners.add(moduleName);
3371
4754
  }
3372
4755
  getModulesOwningFile(filePath) {
3373
4756
  const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
@@ -3383,12 +4766,14 @@ class ModuleDependencyGraph {
3383
4766
  }
3384
4767
  }
3385
4768
  if (directlyAffected.size === 0) {
3386
- return Array.from(this.moduleMap.keys());
4769
+ return [];
3387
4770
  }
3388
4771
  const affected = new Set(directlyAffected);
3389
4772
  const queue = Array.from(directlyAffected);
3390
4773
  while (queue.length > 0) {
3391
4774
  const current = queue.shift();
4775
+ if (!current)
4776
+ continue;
3392
4777
  const dependents = this.dependents.get(current);
3393
4778
  if (dependents) {
3394
4779
  for (const dep of dependents) {
@@ -3416,8 +4801,8 @@ function findAffectedModules(previous, current, changedFiles) {
3416
4801
  // src/watch.ts
3417
4802
  var DEFAULT_DEBOUNCE_MS = 100;
3418
4803
  function watchProject(options) {
3419
- const rootDir = resolve2(options.rootDir);
3420
- const outDir = resolve2(options.outDir);
4804
+ const rootDir = resolve4(options.rootDir);
4805
+ const outDir = resolve4(options.outDir);
3421
4806
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3422
4807
  let timer;
3423
4808
  let closed = false;
@@ -3427,8 +4812,12 @@ function watchProject(options) {
3427
4812
  let watcher;
3428
4813
  const incremental = createIncrementalCompiler();
3429
4814
  let initialEvent;
3430
- let resolveReady;
3431
- let rejectReady;
4815
+ let resolveReady = () => {
4816
+ return;
4817
+ };
4818
+ let rejectReady = () => {
4819
+ return;
4820
+ };
3432
4821
  const ready = new Promise((resolvePromise, rejectPromise) => {
3433
4822
  resolveReady = resolvePromise;
3434
4823
  rejectReady = rejectPromise;
@@ -3497,12 +4886,12 @@ function watchProject(options) {
3497
4886
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
3498
4887
  if (!filename)
3499
4888
  return schedule();
3500
- const changedPath = resolve2(rootDir, filename.toString());
3501
- const relativePath = relative3(outDir, changedPath);
4889
+ const changedPath = resolve4(rootDir, filename.toString());
4890
+ const relativePath = relative4(outDir, changedPath);
3502
4891
  if (!relativePath.startsWith("..") && relativePath !== "")
3503
4892
  return;
3504
4893
  if (/\.(tsx?|mts|cts)$/.test(changedPath))
3505
- schedule(relative3(rootDir, changedPath));
4894
+ schedule(relative4(rootDir, changedPath));
3506
4895
  });
3507
4896
  if (initialEvent)
3508
4897
  resolveReady(initialEvent);
@@ -3523,8 +4912,8 @@ function watchProject(options) {
3523
4912
  };
3524
4913
  }
3525
4914
  // src/inspect.ts
3526
- import { existsSync as existsSync3 } from "node:fs";
3527
- import { join as join4 } from "node:path";
4915
+ import { existsSync as existsSync4 } from "node:fs";
4916
+ import { join as join5 } from "node:path";
3528
4917
  function formatGraph(graph) {
3529
4918
  const lines = [];
3530
4919
  for (const module of graph.modules) {
@@ -3565,13 +4954,13 @@ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
3565
4954
  const checks = [
3566
4955
  {
3567
4956
  name: "project-root",
3568
- ok: existsSync3(rootDir),
3569
- detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
4957
+ ok: existsSync4(rootDir),
4958
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
3570
4959
  },
3571
4960
  {
3572
4961
  name: "tsconfig",
3573
- ok: existsSync3(join4(rootDir, "tsconfig.json")),
3574
- detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
4962
+ ok: existsSync4(join5(rootDir, "tsconfig.json")),
4963
+ detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
3575
4964
  },
3576
4965
  {
3577
4966
  name: "modules",
@@ -3665,12 +5054,15 @@ export {
3665
5054
  MODULAR_MONOLITH_RULES,
3666
5055
  MODULE_BOUNDARY_PROFILES,
3667
5056
  ModuleDependencyGraph,
5057
+ TraitCompiler,
3668
5058
  analyzeProject,
3669
5059
  camelName,
3670
5060
  checkProject,
3671
5061
  compileProject,
5062
+ compileTraits,
3672
5063
  createDependencyGraphCache,
3673
5064
  createIncrementalCompiler,
5065
+ createIncrementalProgramSession,
3674
5066
  doctorProject,
3675
5067
  explainGraph,
3676
5068
  exportGraphDot,
@@ -3681,6 +5073,8 @@ export {
3681
5073
  getModuleBoundaryProfile,
3682
5074
  renderApplication,
3683
5075
  resolveModuleBoundaries,
5076
+ scanGeneratedArtifacts,
5077
+ scanProductionSource,
3684
5078
  validateGraph,
3685
5079
  watchProject
3686
5080
  };