@supacloud/compiler 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,330 @@
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/analyze.ts
10
328
  var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
11
329
  var ROUTE_DECORATORS = {
12
330
  Get: "GET",
@@ -18,63 +336,118 @@ var ROUTE_DECORATORS = {
18
336
  Options: "OPTIONS"
19
337
  };
20
338
  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()));
339
+ function isScope(value) {
340
+ return SCOPES.some((scope) => scope === value);
341
+ }
342
+ function nodeText(node) {
343
+ return node.getText(node.getSourceFile());
344
+ }
345
+ function lineOf(node) {
346
+ const sourceFile = node.getSourceFile();
347
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
348
+ }
349
+ function variableName(decl) {
350
+ return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
351
+ }
352
+ function propertyName(name) {
353
+ if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
354
+ return name.text;
355
+ if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
356
+ return name.text;
357
+ return nodeText(name);
358
+ }
359
+ function parameterName(param) {
360
+ return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
361
+ }
362
+ function decoratorsOf(node) {
363
+ return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
364
+ }
365
+ function decoratorArguments(dec) {
366
+ return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
367
+ }
368
+ function hasMethod(cls, name) {
369
+ return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
370
+ }
371
+ function hasDestroyHook(cls) {
372
+ return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
373
+ }
374
+ function descendantsOfKind(root, predicate) {
375
+ const result = [];
376
+ const visit = (node) => {
377
+ if (predicate(node))
378
+ result.push(node);
379
+ ts3.forEachChild(node, visit);
380
+ };
381
+ visit(root);
382
+ return result;
383
+ }
384
+ async function analyzeProject(rootDir, include, cache, changedPaths) {
385
+ const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
386
+ if (cache)
387
+ cache.programSession = session;
388
+ const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
389
+ const update = session.update(rootNames, changedPaths);
390
+ const program = update.program;
391
+ const checker = program.getTypeChecker();
392
+ const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("/node_modules/") && !sf.fileName.includes("/dist/") && isProjectSourceFile(sf, rootDir)).sort((a, b) => a.fileName.localeCompare(b.fileName));
36
393
  const ctx = {
37
394
  rootDir,
395
+ program,
396
+ checker,
38
397
  tokensByName: new Map,
39
398
  classesByName: new Map,
40
399
  diagnostics: []
41
400
  };
401
+ const nativeTraitFiles = new Map;
402
+ for (const diagnostic of session.getDiagnostics()) {
403
+ ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
404
+ }
405
+ for (const trait of session.getTraits()) {
406
+ const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
407
+ kinds.add(trait.kind);
408
+ nativeTraitFiles.set(trait.file, kinds);
409
+ }
42
410
  for (const sf of sourceFiles) {
43
411
  indexFile(sf, ctx);
44
412
  }
45
413
  const candidates = [];
46
414
  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
- });
415
+ const traits = nativeTraitFiles.get(sf.fileName);
416
+ if (!cache || traits?.has("module")) {
417
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
418
+ const moduleDec = findDecorator(cls, "Module");
419
+ if (!moduleDec)
420
+ continue;
421
+ const options = decoratorObjectArg(moduleDec);
422
+ if (!options)
423
+ continue;
424
+ candidates.push({
425
+ node: cls,
426
+ options,
427
+ className: cls.name?.text ?? "<anonymous>",
428
+ file: sf.fileName,
429
+ line: lineOf(cls)
430
+ });
431
+ }
61
432
  }
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
- });
433
+ if (!cache || traits?.has("defineModule")) {
434
+ for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
435
+ if (nodeText(call.expression) !== "defineModule")
436
+ continue;
437
+ const parent = call.parent;
438
+ if (!parent || !ts3.isVariableDeclaration(parent))
439
+ continue;
440
+ const arg = call.arguments[0];
441
+ if (!arg || !ts3.isObjectLiteralExpression(arg))
442
+ continue;
443
+ candidates.push({
444
+ node: parent,
445
+ options: arg,
446
+ className: variableName(parent),
447
+ file: sf.fileName,
448
+ line: lineOf(parent)
449
+ });
450
+ }
78
451
  }
79
452
  }
80
453
  const nameByNode = new Map;
@@ -87,8 +460,8 @@ async function analyzeProject(rootDir, include, cache) {
87
460
  if (cache) {
88
461
  const currentFileHashes = new Map;
89
462
  for (const sf of sourceFiles) {
90
- const rel = sourcePath(rootDir, sf.getFilePath());
91
- const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
463
+ const rel = sourcePath(rootDir, sf.fileName);
464
+ const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
92
465
  currentFileHashes.set(rel, hash);
93
466
  }
94
467
  const changedFiles = new Set;
@@ -104,7 +477,7 @@ async function analyzeProject(rootDir, include, cache) {
104
477
  }
105
478
  const modulesToKeep = new Map;
106
479
  const finalModules = [];
107
- const finalDiagnostics = [];
480
+ const finalDiagnostics = [...ctx.diagnostics];
108
481
  const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
109
482
  for (const [modName, entry] of cache.modules.entries()) {
110
483
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
@@ -126,14 +499,7 @@ async function analyzeProject(rootDir, include, cache) {
126
499
  const diagBefore = ctx.diagnostics.length;
127
500
  const parsed = parseModule(c, nameByNode, ctx);
128
501
  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);
502
+ const ownedFiles = collectModuleSourceClosure(parsed, ctx);
137
503
  const fileHashes = {};
138
504
  for (const f of ownedFiles) {
139
505
  fileHashes[f] = currentFileHashes.get(f) ?? "";
@@ -160,6 +526,7 @@ async function analyzeProject(rootDir, include, cache) {
160
526
  } else {
161
527
  modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
162
528
  }
529
+ modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
163
530
  const allRegisteredClasses = new Set;
164
531
  const allRegisteredControllers = new Set;
165
532
  const allRegisteredCommands = new Set;
@@ -184,9 +551,9 @@ async function analyzeProject(rootDir, include, cache) {
184
551
  if (!allRegisteredClasses.has(name)) {
185
552
  const injectable = parseInjectableOptions(classInfo.decl, ctx);
186
553
  if (injectable?.providedIn === "root") {
187
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
554
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
188
555
  const file = sourcePath(ctx.rootDir, classInfo.file);
189
- const line = classInfo.decl.getStartLineNumber();
556
+ const line = lineOf(classInfo.decl);
190
557
  if (missing) {
191
558
  warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
192
559
  }
@@ -201,8 +568,9 @@ async function analyzeProject(rootDir, include, cache) {
201
568
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
202
569
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
203
570
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
571
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
204
572
  providedIn: "root",
205
- hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
573
+ hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
206
574
  exported: true,
207
575
  file,
208
576
  line,
@@ -213,8 +581,8 @@ async function analyzeProject(rootDir, include, cache) {
213
581
  if (!allRegisteredControllers.has(name)) {
214
582
  const controllerDec = findDecorator(classInfo.decl, "Controller");
215
583
  if (controllerDec) {
216
- const arg = controllerDec.getArguments()[0];
217
- const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
584
+ const arg = decoratorArguments(controllerDec)[0];
585
+ const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
218
586
  if (isStandalone) {
219
587
  const ctrl = parseController(classInfo.decl, ctx);
220
588
  if (ctrl)
@@ -228,13 +596,14 @@ async function analyzeProject(rootDir, include, cache) {
228
596
  const meta = decoratorObjectArg(commandDec);
229
597
  if (meta && booleanProp(meta, "standalone")) {
230
598
  standaloneCommands.push({
231
- className: classInfo.decl.getName() ?? name,
232
- name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
599
+ className: classInfo.decl.name?.text ?? name,
600
+ name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
233
601
  permission: stringLiteralProp(meta, "permission"),
234
602
  transaction: commandModeProp(meta, "transaction") ?? "none",
235
603
  audit: stringLiteralProp(meta, "audit"),
236
604
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
237
- standalone: true
605
+ standalone: true,
606
+ aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
238
607
  });
239
608
  }
240
609
  }
@@ -259,13 +628,13 @@ async function analyzeProject(rootDir, include, cache) {
259
628
  if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
260
629
  const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
261
630
  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
- }
631
+ modules = modules.map((module) => module === existingRoot ? {
632
+ ...module,
633
+ providers: [...module.providers, ...rootProviders],
634
+ controllers: [...module.controllers, ...standaloneControllers],
635
+ commands: [...module.commands, ...standaloneCommands],
636
+ exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
637
+ } : module);
269
638
  } else {
270
639
  const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
271
640
  modules.unshift({
@@ -304,25 +673,72 @@ async function analyzeProject(rootDir, include, cache) {
304
673
  cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
305
674
  };
306
675
  }
307
- function createProject(rootDir) {
308
- const tsConfigFilePath = join(rootDir, "tsconfig.json");
309
- if (existsSync(tsConfigFilePath)) {
310
- return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
676
+ function collectModuleSourceClosure(module, ctx) {
677
+ const seeds = new Set;
678
+ const addRelativeModule = (path) => {
679
+ if (!path)
680
+ return;
681
+ const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
682
+ seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
683
+ };
684
+ addRelativeModule(module.file);
685
+ for (const provider of module.providers)
686
+ addRelativeModule(provider.importPath);
687
+ for (const controller of module.controllers)
688
+ addRelativeModule(controller.importPath);
689
+ const ownedFiles = new Set;
690
+ const queue = [...seeds];
691
+ while (queue.length > 0) {
692
+ const fileName = queue.shift();
693
+ if (!fileName)
694
+ continue;
695
+ const sourceFile = ctx.program.getSourceFile(fileName);
696
+ if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
697
+ continue;
698
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
699
+ if (ownedFiles.has(relativeFile))
700
+ continue;
701
+ ownedFiles.add(relativeFile);
702
+ for (const statement of sourceFile.statements) {
703
+ let moduleName;
704
+ if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
705
+ moduleName = statement.moduleSpecifier.text;
706
+ } else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
707
+ moduleName = statement.moduleSpecifier.text;
708
+ } else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
709
+ moduleName = statement.moduleReference.expression.text;
710
+ }
711
+ if (!moduleName || moduleName.startsWith("node:"))
712
+ continue;
713
+ const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
714
+ if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
715
+ queue.push(resolved);
716
+ }
311
717
  }
312
- return new Project({
313
- compilerOptions: { experimentalDecorators: true, allowJs: false }
314
- });
718
+ return ownedFiles;
719
+ }
720
+ function resolveSourcePath(rootDir, file) {
721
+ const normalized = file.replace(/\\/g, "/");
722
+ return resolvePath(rootDir, normalized);
723
+ }
724
+ function isProjectSourcePath(fileName, rootDir) {
725
+ const normalized = fileName.replace(/\\/g, "/");
726
+ const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
727
+ return normalized === root || normalized.startsWith(`${root}/`);
728
+ }
729
+ function isProjectSourceFile(sourceFile, rootDir) {
730
+ return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
315
731
  }
316
732
  function indexFile(sf, ctx) {
317
- for (const cls of sf.getClasses()) {
318
- const name = cls.getName();
733
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
734
+ const name = cls.name?.text;
319
735
  if (name && !ctx.classesByName.has(name)) {
320
- ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
736
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
321
737
  }
322
738
  }
323
- for (const statement of sf.getVariableStatements()) {
324
- for (const decl of statement.getDeclarations()) {
325
- const info = parseTokenVariable(decl, sf.getFilePath());
739
+ for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
740
+ for (const decl of statement.declarationList.declarations) {
741
+ const info = parseTokenVariable(decl, sf.fileName);
326
742
  if (info && !ctx.tokensByName.has(info.name)) {
327
743
  ctx.tokensByName.set(info.name, info);
328
744
  }
@@ -330,19 +746,19 @@ function indexFile(sf, ctx) {
330
746
  }
331
747
  }
332
748
  function parseTokenVariable(decl, file) {
333
- const init = decl.getInitializer();
334
- if (!init || !Node.isNewExpression(init))
749
+ const init = decl.initializer;
750
+ if (!init || !ts3.isNewExpression(init))
335
751
  return;
336
- if (init.getExpression().getText() !== "InjectionToken")
752
+ if (nodeText(init.expression) !== "InjectionToken")
337
753
  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();
754
+ const [nameArg, optionsArg] = init.arguments ?? [];
755
+ const info = { name: variableName(decl), file, line: lineOf(decl) };
756
+ if (nameArg && ts3.isStringLiteral(nameArg)) {
757
+ info.stringName = nameArg.text;
342
758
  }
343
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
759
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
344
760
  const scope = stringLiteralProp(optionsArg, "scope");
345
- if (scope && SCOPES.includes(scope)) {
761
+ if (scope && isScope(scope)) {
346
762
  info.scope = scope;
347
763
  }
348
764
  const providedIn = stringLiteralProp(optionsArg, "providedIn");
@@ -359,33 +775,76 @@ function parseTokenVariable(decl, file) {
359
775
  function parseModule(candidate, nameByNode, ctx) {
360
776
  const { options, className, file, line } = candidate;
361
777
  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);
778
+ const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
779
+ const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
363
780
  const imports = arrayProp(options, "imports").map((el) => {
364
781
  const unwrapped = unwrapForwardRef(el);
365
- const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
782
+ const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
366
783
  if (decl) {
367
784
  const known = nameByNode.get(decl);
368
785
  if (known)
369
786
  return known;
370
- if (Node.isClassDeclaration(decl)) {
787
+ if (ts3.isClassDeclaration(decl)) {
371
788
  const dec = findDecorator(decl, "Module");
372
789
  const decOptions = dec && decoratorObjectArg(dec);
373
790
  const decName = decOptions && stringLiteralProp(decOptions, "name");
374
- return decName ?? decl.getName() ?? el.getText();
791
+ return decName ?? decl.name?.text ?? nodeText(el);
375
792
  }
376
- if (Node.isVariableDeclaration(decl))
377
- return decl.getName();
793
+ if (ts3.isVariableDeclaration(decl))
794
+ return variableName(decl);
378
795
  }
379
- return el.getText();
796
+ return nodeText(el);
380
797
  }).filter((v, i, arr) => arr.indexOf(v) === i);
381
798
  const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
382
799
  const exportsSet = new Set(exports);
383
800
  const providers = [];
384
- for (const el of arrayProp(options, "providers")) {
801
+ for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
802
+ const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
803
+ if (parsedProviders) {
804
+ providers.push(...parsedProviders);
805
+ continue;
806
+ }
807
+ if (ts3.isCallExpression(el)) {
808
+ const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
809
+ warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
810
+ continue;
811
+ }
385
812
  const provider = parseProvider(el, exportsSet, ctx);
386
813
  if (provider)
387
814
  providers.push(provider);
388
815
  }
816
+ for (const el of arrayProp(options, "jobs")) {
817
+ if (!ts3.isIdentifier(el))
818
+ continue;
819
+ const decl = resolveDeclaration(el, ctx)[0];
820
+ if (!decl || !ts3.isClassDeclaration(decl))
821
+ continue;
822
+ const className2 = decl.name?.text ?? el.text;
823
+ const registeredProvider = providers.find((provider) => provider.token === className2);
824
+ if (registeredProvider)
825
+ continue;
826
+ const deps = classDeps(decl, ctx);
827
+ const injectable = parseInjectableOptions(decl, ctx);
828
+ const scope = injectable?.scope === "application" ? "application" : "job";
829
+ providers.push({
830
+ token: className2,
831
+ tokenKind: "class",
832
+ kind: "class",
833
+ useClass: className2,
834
+ scope,
835
+ deps: deps.deps,
836
+ optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
837
+ selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
838
+ skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
839
+ hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
840
+ functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
841
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
842
+ exported: exportsSet.has(className2),
843
+ file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
844
+ line: lineOf(decl),
845
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
846
+ });
847
+ }
389
848
  const controllers = [];
390
849
  for (const el of arrayProp(options, "controllers")) {
391
850
  const controller = parseController(el, ctx);
@@ -395,40 +854,59 @@ function parseModule(candidate, nameByNode, ctx) {
395
854
  const handlerClasses = [];
396
855
  const seenHandlers = new Set;
397
856
  const collectHandler = (expr) => {
398
- if (!Node.isIdentifier(expr))
857
+ if (!ts3.isIdentifier(expr))
399
858
  return;
400
- const decl = resolveDeclaration(expr)[0];
401
- if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
402
- seenHandlers.add(decl.getName() ?? "");
859
+ const decl = resolveDeclaration(expr, ctx)[0];
860
+ if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
861
+ seenHandlers.add(decl.name?.text ?? "");
403
862
  handlerClasses.push(decl);
404
863
  }
405
864
  };
406
865
  for (const el of arrayProp(options, "providers")) {
407
- if (Node.isIdentifier(el))
866
+ if (ts3.isIdentifier(el))
408
867
  collectHandler(el);
409
- if (Node.isObjectLiteralExpression(el)) {
868
+ if (ts3.isObjectLiteralExpression(el)) {
410
869
  const useClass = getProp(el, "useClass");
411
870
  if (useClass)
412
871
  collectHandler(useClass);
413
872
  }
414
873
  }
415
874
  arrayProp(options, "commands").forEach(collectHandler);
875
+ arrayProp(options, "jobs").forEach(collectHandler);
416
876
  arrayProp(options, "queries").forEach(collectHandler);
417
877
  const commands = [];
878
+ const jobs = [];
418
879
  const queries = [];
419
880
  for (const cls of handlerClasses) {
420
881
  const commandDec = findDecorator(cls, "Command");
421
882
  if (commandDec) {
422
883
  const meta = decoratorObjectArg(commandDec);
423
884
  if (meta) {
885
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
424
886
  commands.push({
425
- className: cls.getName() ?? "<anonymous>",
426
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
887
+ className: cls.name?.text ?? "<anonymous>",
888
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
427
889
  permission: stringLiteralProp(meta, "permission"),
428
890
  transaction: commandModeProp(meta, "transaction") ?? "none",
429
891
  audit: stringLiteralProp(meta, "audit"),
430
892
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
431
- standalone: booleanProp(meta, "standalone") || undefined
893
+ ...booleanProp(meta, "standalone") ? { standalone: true } : {},
894
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
895
+ });
896
+ }
897
+ }
898
+ const jobDec = findDecorator(cls, "Job");
899
+ if (jobDec) {
900
+ const meta = decoratorObjectArg(jobDec);
901
+ if (meta) {
902
+ const injectable = parseInjectableOptions(cls, ctx);
903
+ const provider = providers.find((candidate2) => candidate2.token === (cls.name?.text ?? "<anonymous>"));
904
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${cls.name?.text ?? "<anonymous>"}`);
905
+ jobs.push({
906
+ className: cls.name?.text ?? "<anonymous>",
907
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
908
+ scope: provider?.scope ?? (injectable?.scope === "application" ? "application" : "job"),
909
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
432
910
  });
433
911
  }
434
912
  }
@@ -437,8 +915,8 @@ function parseModule(candidate, nameByNode, ctx) {
437
915
  const meta = decoratorObjectArg(queryDec);
438
916
  if (meta) {
439
917
  queries.push({
440
- className: cls.getName() ?? "<anonymous>",
441
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
918
+ className: cls.name?.text ?? "<anonymous>",
919
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
442
920
  });
443
921
  }
444
922
  }
@@ -453,7 +931,9 @@ function parseModule(candidate, nameByNode, ctx) {
453
931
  providers,
454
932
  controllers,
455
933
  commands,
934
+ jobs,
456
935
  queries,
936
+ ...aspects.length > 0 ? { aspects } : {},
457
937
  exports
458
938
  };
459
939
  }
@@ -462,14 +942,14 @@ function commandModeProp(object, name) {
462
942
  return value === "required" || value === "none" ? value : undefined;
463
943
  }
464
944
  function parseProvider(el, exportsSet, ctx) {
465
- const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
466
- const line = el.getStartLineNumber();
945
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
946
+ const line = lineOf(el);
467
947
  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 };
948
+ if (ts3.isIdentifier(unwrappedEl)) {
949
+ const decl = resolveDeclaration(unwrappedEl, ctx)[0];
950
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
951
+ const className = cls?.name?.text ?? unwrappedEl.text;
952
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
473
953
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
474
954
  if (missing) {
475
955
  warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
@@ -485,15 +965,16 @@ function parseProvider(el, exportsSet, ctx) {
485
965
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
486
966
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
487
967
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
968
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
488
969
  providedIn: injectable?.providedIn,
489
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
970
+ hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
490
971
  exported: exportsSet.has(className),
491
972
  file,
492
973
  line,
493
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
974
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
494
975
  };
495
976
  }
496
- if (!Node.isObjectLiteralExpression(el))
977
+ if (!ts3.isObjectLiteralExpression(el))
497
978
  return;
498
979
  const provideExpr = getProp(el, "provide");
499
980
  if (!provideExpr)
@@ -508,26 +989,36 @@ function parseProvider(el, exportsSet, ctx) {
508
989
  const useExistingExpr = getProp(el, "useExisting");
509
990
  if (useClassExpr) {
510
991
  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();
992
+ const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
993
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
994
+ const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
514
995
  let deps = explicitDeps;
515
996
  let optionalDeps = [];
516
997
  let selfDeps = [];
517
998
  let skipSelfDeps = [];
518
999
  let hostDeps = [];
519
- if (deps.length === 0 && cls) {
1000
+ let functionalInjects = [];
1001
+ if (cls) {
520
1002
  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;
1003
+ if (deps.length === 0) {
1004
+ deps = result.deps;
1005
+ optionalDeps = result.optionalDeps;
1006
+ selfDeps = result.selfDeps;
1007
+ skipSelfDeps = result.skipSelfDeps;
1008
+ hostDeps = result.hostDeps;
1009
+ } else {
1010
+ optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
1011
+ selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
1012
+ skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
1013
+ hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
1014
+ }
1015
+ functionalInjects = result.functionalInjects;
526
1016
  if (result.missing) {
527
1017
  warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
528
1018
  }
529
1019
  }
530
1020
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
1021
+ validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
531
1022
  return {
532
1023
  token,
533
1024
  tokenKind,
@@ -539,35 +1030,38 @@ function parseProvider(el, exportsSet, ctx) {
539
1030
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
540
1031
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
541
1032
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1033
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
542
1034
  multi: multi ?? undefined,
543
1035
  providedIn: injectable?.providedIn,
544
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1036
+ hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
545
1037
  exported: exportsSet.has(token),
546
1038
  file,
547
1039
  line,
548
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1040
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
549
1041
  };
550
1042
  }
551
1043
  if (useValueExpr) {
1044
+ validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
552
1045
  return {
553
1046
  token,
554
1047
  tokenKind,
555
1048
  kind: "value",
556
- useValueExpr: useValueExpr.getText(),
1049
+ useValueExpr: nodeText(useValueExpr),
557
1050
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
558
1051
  deps: [],
559
1052
  multi: multi ?? undefined,
560
1053
  exported: exportsSet.has(token),
561
1054
  file,
562
1055
  line,
563
- importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
1056
+ importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
564
1057
  };
565
1058
  }
566
1059
  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();
1060
+ const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
1061
+ const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
1062
+ return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
1063
+ })() : nodeText(useFactoryExpr);
1064
+ validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
571
1065
  return {
572
1066
  token,
573
1067
  tokenKind,
@@ -579,11 +1073,12 @@ function parseProvider(el, exportsSet, ctx) {
579
1073
  exported: exportsSet.has(token),
580
1074
  file,
581
1075
  line,
582
- importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
1076
+ importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
583
1077
  };
584
1078
  }
585
1079
  if (useExistingExpr) {
586
1080
  const target = tokenNameOf(useExistingExpr, ctx).name;
1081
+ validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
587
1082
  return {
588
1083
  token,
589
1084
  tokenKind,
@@ -599,14 +1094,262 @@ function parseProvider(el, exportsSet, ctx) {
599
1094
  }
600
1095
  return;
601
1096
  }
1097
+ function expandProviderExpressions(expressions, ctx, seen = new Set) {
1098
+ const result = [];
1099
+ for (const expression of expressions) {
1100
+ if (ts3.isSpreadElement(expression)) {
1101
+ result.push(...expandProviderExpressions([expression.expression], ctx, seen));
1102
+ continue;
1103
+ }
1104
+ if (ts3.isIdentifier(expression)) {
1105
+ const declaration = resolveDeclaration(expression, ctx)[0];
1106
+ if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
1107
+ const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
1108
+ if (seen.has(key))
1109
+ continue;
1110
+ const initializer = declaration.initializer;
1111
+ if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
1112
+ const nested = initializer.arguments[0];
1113
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1114
+ seen.add(key);
1115
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1116
+ seen.delete(key);
1117
+ continue;
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
1123
+ const nested = expression.arguments[0];
1124
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1125
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1126
+ continue;
1127
+ }
1128
+ }
1129
+ result.push(expression);
1130
+ }
1131
+ return result;
1132
+ }
1133
+ function isProviderHelper(expression, name) {
1134
+ return nodeText(expression.expression).split(".").pop() === name;
1135
+ }
1136
+ function parseFunctionalProvider(expression, exportsSet, ctx) {
1137
+ if (!ts3.isCallExpression(expression))
1138
+ return;
1139
+ const helper = nodeText(expression.expression).split(".").pop();
1140
+ const args = expression.arguments;
1141
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1142
+ const line = lineOf(expression);
1143
+ if (helper === "provideToken") {
1144
+ const tokenExpr = args[0];
1145
+ const valueExpr = args[1];
1146
+ if (!tokenExpr || !valueExpr)
1147
+ return [];
1148
+ const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
1149
+ validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
1150
+ return [{
1151
+ token,
1152
+ tokenKind,
1153
+ kind: "value",
1154
+ useValueExpr: nodeText(valueExpr),
1155
+ scope: resolveScope({ tokenName: token }, ctx),
1156
+ deps: [],
1157
+ exported: exportsSet.has(token),
1158
+ file,
1159
+ line,
1160
+ importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
1161
+ }];
1162
+ }
1163
+ if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
1164
+ const initializer = args[0];
1165
+ if (!initializer)
1166
+ return [];
1167
+ const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
1168
+ return [{
1169
+ token,
1170
+ tokenKind: "injection-token",
1171
+ kind: "value",
1172
+ useValueExpr: nodeText(initializer),
1173
+ scope: "application",
1174
+ deps: [],
1175
+ multi: true,
1176
+ exported: false,
1177
+ file,
1178
+ line,
1179
+ importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
1180
+ }];
1181
+ }
1182
+ if (helper === "provideRouter") {
1183
+ const providers = [];
1184
+ const routes = args[0];
1185
+ if (routes) {
1186
+ providers.push({
1187
+ token: "ROUTE_CONFIG",
1188
+ tokenKind: "injection-token",
1189
+ kind: "value",
1190
+ useValueExpr: nodeText(routes),
1191
+ scope: "application",
1192
+ deps: [],
1193
+ exported: false,
1194
+ file,
1195
+ line,
1196
+ importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
1197
+ });
1198
+ }
1199
+ for (const feature of args.slice(1)) {
1200
+ if (!ts3.isCallExpression(feature))
1201
+ continue;
1202
+ const featureName = nodeText(feature.expression).split(".").pop();
1203
+ if (featureName === "withRouterConfig" && feature.arguments[0]) {
1204
+ providers.push({
1205
+ token: "ROUTER_CONFIGURATION",
1206
+ tokenKind: "injection-token",
1207
+ kind: "value",
1208
+ useValueExpr: nodeText(feature.arguments[0]),
1209
+ scope: "application",
1210
+ deps: [],
1211
+ exported: false,
1212
+ file,
1213
+ line
1214
+ });
1215
+ } else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
1216
+ const strategy = feature.arguments[0];
1217
+ const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
1218
+ providers.push({
1219
+ token: "TITLE_STRATEGY",
1220
+ tokenKind: "injection-token",
1221
+ kind: isClass ? "class" : "value",
1222
+ ...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
1223
+ scope: "application",
1224
+ deps: [],
1225
+ exported: false,
1226
+ file,
1227
+ line,
1228
+ importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
1229
+ });
1230
+ }
1231
+ }
1232
+ return providers;
1233
+ }
1234
+ if (helper === "provideHttpClient") {
1235
+ const providers = [{
1236
+ token: "HttpClient",
1237
+ tokenKind: "class",
1238
+ kind: "class",
1239
+ useClass: "HttpClient",
1240
+ scope: "application",
1241
+ deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1242
+ optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1243
+ exported: false,
1244
+ file,
1245
+ line,
1246
+ importModule: "@supacloud/app"
1247
+ }];
1248
+ for (const feature of args) {
1249
+ if (!ts3.isCallExpression(feature))
1250
+ continue;
1251
+ const featureName = nodeText(feature.expression).split(".").pop();
1252
+ if (featureName === "withInterceptors") {
1253
+ for (const interceptorArg of feature.arguments) {
1254
+ const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
1255
+ for (const value of values) {
1256
+ providers.push({
1257
+ token: "HTTP_INTERCEPTORS",
1258
+ tokenKind: "injection-token",
1259
+ kind: "value",
1260
+ useValueExpr: nodeText(value),
1261
+ scope: "application",
1262
+ deps: [],
1263
+ multi: true,
1264
+ exported: false,
1265
+ file,
1266
+ line,
1267
+ importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
1268
+ });
1269
+ }
1270
+ }
1271
+ } else if (featureName === "withFetch" && feature.arguments.length > 0) {
1272
+ warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
1273
+ }
1274
+ }
1275
+ return providers;
1276
+ }
1277
+ return;
1278
+ }
1279
+ function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
1280
+ const expected = providerTokenValueType(provideExpr, ctx);
1281
+ const actual = providerImplementationType(implementationExpr, kind, ctx);
1282
+ if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
1283
+ return;
1284
+ if (ctx.checker.isTypeAssignableTo(actual, expected))
1285
+ return;
1286
+ const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
1287
+ ctx.diagnostics.push({
1288
+ severity: "error",
1289
+ code: "provider-type-mismatch",
1290
+ message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
1291
+ file,
1292
+ line,
1293
+ errorCode: "SC2010",
1294
+ docsUrl: "https://supacloud.dev/errors/SC2010"
1295
+ });
1296
+ }
1297
+ function providerTokenValueType(expr, ctx) {
1298
+ const type = ctx.checker.getTypeAtLocation(expr);
1299
+ const typeArguments = typeArgumentsOf(type, ctx);
1300
+ if (typeArguments.length > 0)
1301
+ return typeArguments[0];
1302
+ if (ts3.isIdentifier(expr)) {
1303
+ const declaration = resolveDeclaration(expr, ctx)[0];
1304
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1305
+ return declaredClassType(declaration, ctx);
1306
+ }
1307
+ }
1308
+ return;
1309
+ }
1310
+ function providerImplementationType(expr, kind, ctx) {
1311
+ if (kind === "class" || kind === "existing") {
1312
+ if (ts3.isIdentifier(expr)) {
1313
+ const declaration = resolveDeclaration(expr, ctx)[0];
1314
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1315
+ return declaredClassType(declaration, ctx);
1316
+ }
1317
+ }
1318
+ const type = ctx.checker.getTypeAtLocation(expr);
1319
+ const typeArguments = typeArgumentsOf(type, ctx);
1320
+ return typeArguments.length > 0 ? typeArguments[0] : undefined;
1321
+ }
1322
+ if (kind === "factory") {
1323
+ const type = ctx.checker.getTypeAtLocation(expr);
1324
+ const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
1325
+ return signature?.getReturnType();
1326
+ }
1327
+ return ctx.checker.getTypeAtLocation(expr);
1328
+ }
1329
+ function declaredClassType(declaration, ctx) {
1330
+ const name = declaration.name;
1331
+ if (!name)
1332
+ return;
1333
+ const symbol = ctx.checker.getSymbolAtLocation(name);
1334
+ return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
1335
+ }
1336
+ function typeArgumentsOf(type, ctx) {
1337
+ return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
1338
+ }
1339
+ function isTypeReference(type) {
1340
+ return "target" in type;
1341
+ }
1342
+ function isUnknownOrAny(type) {
1343
+ return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
1344
+ }
602
1345
  function parseController(input, ctx) {
603
1346
  let decl;
604
- if (Node.isClassDeclaration(input)) {
1347
+ if (ts3.isClassDeclaration(input)) {
605
1348
  decl = input;
606
1349
  } else {
607
1350
  const unwrapped = unwrapForwardRef(input);
608
- const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
609
- if (resolved && Node.isClassDeclaration(resolved)) {
1351
+ const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
1352
+ if (resolved && ts3.isClassDeclaration(resolved)) {
610
1353
  decl = resolved;
611
1354
  }
612
1355
  }
@@ -617,46 +1360,46 @@ function parseController(input, ctx) {
617
1360
  return;
618
1361
  let path = "/";
619
1362
  let standalone;
620
- const pathArg = controllerDec.getArguments()[0];
1363
+ const pathArg = decoratorArguments(controllerDec)[0];
621
1364
  if (pathArg) {
622
- if (Node.isStringLiteral(pathArg)) {
623
- path = pathArg.getLiteralText();
624
- } else if (Node.isObjectLiteralExpression(pathArg)) {
1365
+ if (ts3.isStringLiteral(pathArg)) {
1366
+ path = pathArg.text;
1367
+ } else if (ts3.isObjectLiteralExpression(pathArg)) {
625
1368
  const p = stringLiteralProp(pathArg, "path");
626
1369
  if (p)
627
1370
  path = p;
628
1371
  standalone = booleanProp(pathArg, "standalone");
629
1372
  }
630
1373
  }
631
- const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
632
- const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1374
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
1375
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
633
1376
  if (missing) {
634
- warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
1377
+ warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
635
1378
  }
636
1379
  const injectable = parseInjectableOptions(decl, ctx);
637
1380
  const routes = [];
638
1381
  const schemaImports = {};
639
1382
  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));
1383
+ for (const dec of decoratorsOf(decl)) {
1384
+ if (decoratorName2(dec) === "UseGuards") {
1385
+ for (const gArg of decoratorArguments(dec)) {
1386
+ classGuards.push(tokenText(gArg, ctx));
644
1387
  }
645
1388
  }
646
1389
  }
647
- for (const method of decl.getMethods()) {
648
- for (const dec of method.getDecorators()) {
649
- const name = decoratorName(dec);
1390
+ for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
1391
+ for (const dec of decoratorsOf(method)) {
1392
+ const name = decoratorName2(dec);
650
1393
  const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
651
1394
  if (!httpMethod)
652
1395
  continue;
653
- const args = dec.getArguments();
1396
+ const args = decoratorArguments(dec);
654
1397
  const pathArg2 = args[0];
655
- const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
1398
+ const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
656
1399
  const route = {
657
1400
  method: httpMethod,
658
1401
  path: routePath,
659
- handler: method.getName()
1402
+ handler: propertyName(method.name)
660
1403
  };
661
1404
  const pathParams = [];
662
1405
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
@@ -674,13 +1417,13 @@ function parseController(input, ctx) {
674
1417
  const queryDefaults = {};
675
1418
  let hasBodyBinding = false;
676
1419
  const handlerParams = [];
677
- for (const p of method.getParameters()) {
678
- const pName = p.getName();
1420
+ for (const p of method.parameters) {
1421
+ const pName = parameterName(p);
679
1422
  let hasBindingDecorator = false;
680
1423
  let paramNode;
681
- for (const pDec of p.getDecorators()) {
682
- const dName = decoratorName(pDec);
683
- const dArgs = pDec.getArguments();
1424
+ for (const pDec of decoratorsOf(p)) {
1425
+ const dName = decoratorName2(pDec);
1426
+ const dArgs = decoratorArguments(pDec);
684
1427
  if (dName === "Param") {
685
1428
  hasBindingDecorator = true;
686
1429
  const parsed = parseBindingOptions(dArgs, pName);
@@ -722,7 +1465,7 @@ function parseController(input, ctx) {
722
1465
  }
723
1466
  if (!hasBindingDecorator && pathParams.includes(pName)) {
724
1467
  paramBindings.push(pName);
725
- const typeText = p.getType().getText();
1468
+ const typeText = p.type ? nodeText(p.type) : "";
726
1469
  let inferredTransform;
727
1470
  if (typeText === "number") {
728
1471
  paramTransforms[pName] = "number";
@@ -765,37 +1508,37 @@ function parseController(input, ctx) {
765
1508
  route.handlerParams = handlerParams;
766
1509
  const routeGuards = [...classGuards];
767
1510
  const routeCanDeactivate = [];
768
- for (const mDec of method.getDecorators()) {
769
- const dName = decoratorName(mDec);
770
- const mArgs = mDec.getArguments();
1511
+ for (const mDec of decoratorsOf(method)) {
1512
+ const dName = decoratorName2(mDec);
1513
+ const mArgs = decoratorArguments(mDec);
771
1514
  if (dName === "UseGuards") {
772
1515
  for (const gArg of mArgs) {
773
- routeGuards.push(tokenText(gArg));
1516
+ routeGuards.push(tokenText(gArg, ctx));
774
1517
  }
775
1518
  } else if (dName === "CanDeactivate") {
776
1519
  for (const gArg of mArgs) {
777
- routeCanDeactivate.push(tokenText(gArg));
1520
+ routeCanDeactivate.push(tokenText(gArg, ctx));
778
1521
  }
779
1522
  } else if (dName === "Title") {
780
1523
  const tArg = mArgs[0];
781
- if (tArg && Node.isStringLiteral(tArg)) {
782
- route.title = tArg.getLiteralText();
1524
+ if (tArg && ts3.isStringLiteral(tArg)) {
1525
+ route.title = tArg.text;
783
1526
  }
784
1527
  } else if (dName === "Data") {
785
1528
  const dArg = mArgs[0];
786
- if (dArg && Node.isObjectLiteralExpression(dArg)) {
1529
+ if (dArg && ts3.isObjectLiteralExpression(dArg)) {
787
1530
  route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
788
1531
  }
789
1532
  } else if (dName === "Resolve") {
790
1533
  const rArg = mArgs[0];
791
- if (rArg && Node.isObjectLiteralExpression(rArg)) {
1534
+ if (rArg && ts3.isObjectLiteralExpression(rArg)) {
792
1535
  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();
1536
+ for (const prop of rArg.properties) {
1537
+ if (ts3.isPropertyAssignment(prop)) {
1538
+ const rName = propertyName(prop.name);
1539
+ const init = prop.initializer;
797
1540
  if (init)
798
- resolvers[rName] = tokenText(init);
1541
+ resolvers[rName] = tokenText(init, ctx);
799
1542
  }
800
1543
  }
801
1544
  if (Object.keys(resolvers).length > 0) {
@@ -805,52 +1548,52 @@ function parseController(input, ctx) {
805
1548
  }
806
1549
  }
807
1550
  const optionsArg = args[1];
808
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
1551
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
809
1552
  for (const field of ["body", "params", "query", "response"]) {
810
1553
  const schemaExpr = getProp(optionsArg, field);
811
- if (schemaExpr && Node.isIdentifier(schemaExpr)) {
812
- route[field] = schemaExpr.getText();
1554
+ if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
1555
+ route[field] = nodeText(schemaExpr);
813
1556
  const importPath = importPathOf(schemaExpr, ctx);
814
1557
  if (importPath)
815
- schemaImports[schemaExpr.getText()] = importPath;
1558
+ schemaImports[schemaExpr.text] = importPath;
816
1559
  }
817
1560
  }
818
1561
  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();
1562
+ if (commandExpr && ts3.isIdentifier(commandExpr)) {
1563
+ const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
1564
+ route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
822
1565
  }
823
1566
  const guardsExpr = getProp(optionsArg, "guards");
824
- if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
825
- for (const el of guardsExpr.getElements()) {
826
- routeGuards.push(tokenText(el));
1567
+ if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
1568
+ for (const el of guardsExpr.elements) {
1569
+ routeGuards.push(tokenText(el, ctx));
827
1570
  }
828
1571
  }
829
1572
  const canMatchExpr = getProp(optionsArg, "canMatch");
830
- if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
1573
+ if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
831
1574
  const canMatchList = [];
832
- for (const el of canMatchExpr.getElements()) {
833
- canMatchList.push(tokenText(el));
1575
+ for (const el of canMatchExpr.elements) {
1576
+ canMatchList.push(tokenText(el, ctx));
834
1577
  }
835
1578
  if (canMatchList.length > 0) {
836
1579
  route.canMatch = canMatchList;
837
1580
  }
838
1581
  }
839
1582
  const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
840
- if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
841
- for (const el of canDeactivateExpr.getElements()) {
842
- routeCanDeactivate.push(tokenText(el));
1583
+ if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
1584
+ for (const el of canDeactivateExpr.elements) {
1585
+ routeCanDeactivate.push(tokenText(el, ctx));
843
1586
  }
844
1587
  }
845
1588
  const resolversExpr = getProp(optionsArg, "resolvers");
846
- if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
1589
+ if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
847
1590
  const resolvers = {};
848
- for (const prop of resolversExpr.getProperties()) {
849
- if (Node.isPropertyAssignment(prop)) {
850
- const rName = prop.getName();
851
- const init = prop.getInitializer();
1591
+ for (const prop of resolversExpr.properties) {
1592
+ if (ts3.isPropertyAssignment(prop)) {
1593
+ const rName = propertyName(prop.name);
1594
+ const init = prop.initializer;
852
1595
  if (init)
853
- resolvers[rName] = tokenText(init);
1596
+ resolvers[rName] = tokenText(init, ctx);
854
1597
  }
855
1598
  }
856
1599
  if (Object.keys(resolvers).length > 0) {
@@ -858,24 +1601,27 @@ function parseController(input, ctx) {
858
1601
  }
859
1602
  }
860
1603
  const redirectToExpr = getProp(optionsArg, "redirectTo");
861
- if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
862
- route.redirectTo = redirectToExpr.getLiteralText();
1604
+ if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
1605
+ route.redirectTo = redirectToExpr.text;
863
1606
  }
864
1607
  const pathMatchExpr = getProp(optionsArg, "pathMatch");
865
- if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
866
- const val = pathMatchExpr.getLiteralText();
1608
+ if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
1609
+ const val = pathMatchExpr.text;
867
1610
  if (val === "full" || val === "prefix") {
868
1611
  route.pathMatch = val;
869
1612
  }
870
1613
  }
871
1614
  const titleExpr = getProp(optionsArg, "title");
872
- if (titleExpr && Node.isStringLiteral(titleExpr)) {
873
- route.title = titleExpr.getLiteralText();
1615
+ if (titleExpr && ts3.isStringLiteral(titleExpr)) {
1616
+ route.title = titleExpr.text;
874
1617
  }
875
1618
  const dataExpr = getProp(optionsArg, "data");
876
- if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
1619
+ if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
877
1620
  route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
878
1621
  }
1622
+ const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
1623
+ if (aspects.length > 0)
1624
+ route.aspects = aspects;
879
1625
  }
880
1626
  if (routeGuards.length > 0) {
881
1627
  route.guards = routeGuards;
@@ -887,46 +1633,40 @@ function parseController(input, ctx) {
887
1633
  }
888
1634
  }
889
1635
  return {
890
- className: decl.getName() ?? "<anonymous>",
1636
+ className: decl.name?.text ?? "<anonymous>",
891
1637
  path,
892
1638
  scope: injectable?.scope ?? "request",
893
1639
  deps,
1640
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
894
1641
  optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
895
1642
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
896
1643
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1644
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1645
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
897
1646
  standalone: standalone || undefined,
898
1647
  routes,
899
1648
  file,
900
- importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
1649
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
901
1650
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
902
1651
  };
903
1652
  }
904
1653
  function classDeps(cls, ctx) {
905
1654
  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 = [];
1655
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1656
+ const deps = injectable?.deps ? [...injectable.deps] : [];
918
1657
  const optionalDeps = [];
919
1658
  const selfDeps = [];
920
1659
  const skipSelfDeps = [];
921
1660
  const hostDeps = [];
1661
+ const functionalInjects = [];
922
1662
  let missing = false;
923
- if (ctor && ctor.getParameters().length > 0) {
924
- const injectParams = parseInjectParams(cls);
1663
+ if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
1664
+ const injectParams = parseInjectParams(cls, ctx);
925
1665
  const optionalIndices = parseOptionalParams(cls);
926
1666
  const selfIndices = parseModifierParams(cls, "Self");
927
1667
  const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
928
1668
  const hostIndices = parseModifierParams(cls, "Host");
929
- ctor.getParameters().forEach((param, index) => {
1669
+ ctor.parameters.forEach((param, index) => {
930
1670
  const isOptional = optionalIndices.has(index);
931
1671
  const injected = injectParams.get(index);
932
1672
  const tokenName = injected ?? paramTypeTokenName(param, ctx);
@@ -946,47 +1686,62 @@ function classDeps(cls, ctx) {
946
1686
  }
947
1687
  });
948
1688
  }
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();
1689
+ for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
1690
+ const init = prop.initializer;
1691
+ if (init && ts3.isCallExpression(init)) {
1692
+ const callName = nodeText(init.expression).split(".").pop();
953
1693
  if (callName === "inject") {
954
- const [tokenArg, optionsArg] = init.getArguments();
1694
+ const [tokenArg, optionsArg] = init.arguments;
955
1695
  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
- }
1696
+ const tokenName = tokenText(tokenArg, ctx);
1697
+ const unwrappedToken = unwrapForwardRef(tokenArg);
1698
+ const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
1699
+ if (!known) {
1700
+ missing = true;
1701
+ continue;
1702
+ }
1703
+ if (!deps.includes(tokenName))
1704
+ deps.push(tokenName);
1705
+ const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
1706
+ optional: booleanProp(optionsArg, "optional") ?? false,
1707
+ self: booleanProp(optionsArg, "self") ?? false,
1708
+ skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
1709
+ host: booleanProp(optionsArg, "host") ?? false
1710
+ } : { optional: false, self: false, skipSelf: false, host: false };
1711
+ if (options.optional && !optionalDeps.includes(tokenName))
1712
+ optionalDeps.push(tokenName);
1713
+ if (options.self && !selfDeps.includes(tokenName))
1714
+ selfDeps.push(tokenName);
1715
+ if (options.skipSelf && !skipSelfDeps.includes(tokenName))
1716
+ skipSelfDeps.push(tokenName);
1717
+ if (options.host && !hostDeps.includes(tokenName))
1718
+ hostDeps.push(tokenName);
1719
+ if (!functionalInjects.some((entry) => entry.token === tokenName)) {
1720
+ functionalInjects.push({
1721
+ token: tokenName,
1722
+ expression: nodeText(unwrappedToken),
1723
+ importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
1724
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1725
+ return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
1726
+ })() : undefined,
1727
+ importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
1728
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1729
+ return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
1730
+ })() : undefined,
1731
+ ...options
1732
+ });
978
1733
  }
979
1734
  }
980
1735
  }
981
1736
  }
982
1737
  }
983
- return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
1738
+ return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
984
1739
  }
985
1740
  function paramTypeTokenName(param, ctx) {
986
- const typeNode = param.getTypeNode();
1741
+ const typeNode = param.type;
987
1742
  if (!typeNode)
988
1743
  return;
989
- const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
1744
+ const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
990
1745
  if (ctx.classesByName.has(text))
991
1746
  return text;
992
1747
  if (ctx.tokensByName.has(text))
@@ -1004,63 +1759,63 @@ function parseInjectableOptions(cls, ctx) {
1004
1759
  const providedIn = stringLiteralProp(obj, "providedIn");
1005
1760
  const depsExpr = getProp(obj, "deps");
1006
1761
  return {
1007
- scope: scope && SCOPES.includes(scope) ? scope : undefined,
1762
+ scope: scope && isScope(scope) ? scope : undefined,
1008
1763
  providedIn: providedIn === "root" ? "root" : undefined,
1009
- deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
1764
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
1010
1765
  };
1011
1766
  }
1012
- function parseInjectParams(cls) {
1767
+ function parseInjectParams(cls, ctx) {
1013
1768
  const result = new Map;
1014
- const ctor = cls.getConstructors()[0];
1769
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1015
1770
  if (!ctor)
1016
1771
  return result;
1017
- ctor.getParameters().forEach((param, index) => {
1018
- for (const dec of param.getDecorators()) {
1019
- if (decoratorName(dec) !== "Inject")
1772
+ ctor.parameters.forEach((param, index) => {
1773
+ for (const dec of decoratorsOf(param)) {
1774
+ if (decoratorName2(dec) !== "Inject")
1020
1775
  continue;
1021
- const arg = dec.getArguments()[0];
1776
+ const arg = decoratorArguments(dec)[0];
1022
1777
  if (arg)
1023
- result.set(index, tokenText(arg));
1778
+ result.set(index, tokenText(arg, ctx));
1024
1779
  }
1025
1780
  });
1026
1781
  return result;
1027
1782
  }
1028
1783
  function parseOptionalParams(cls) {
1029
1784
  const result = new Set;
1030
- const ctor = cls.getConstructors()[0];
1785
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1031
1786
  if (!ctor)
1032
1787
  return result;
1033
- ctor.getParameters().forEach((param, index) => {
1034
- for (const dec of param.getDecorators()) {
1035
- if (decoratorName(dec) === "Optional")
1788
+ ctor.parameters.forEach((param, index) => {
1789
+ for (const dec of decoratorsOf(param)) {
1790
+ if (decoratorName2(dec) === "Optional")
1036
1791
  result.add(index);
1037
1792
  }
1038
- if (param.hasQuestionToken())
1793
+ if (param.questionToken)
1039
1794
  result.add(index);
1040
1795
  });
1041
1796
  return result;
1042
1797
  }
1043
1798
  function parseModifierParams(cls, modifierName) {
1044
1799
  const result = new Set;
1045
- const ctor = cls.getConstructors()[0];
1800
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1046
1801
  if (!ctor)
1047
1802
  return result;
1048
- ctor.getParameters().forEach((param, index) => {
1049
- for (const dec of param.getDecorators()) {
1050
- if (decoratorName(dec) === modifierName)
1803
+ ctor.parameters.forEach((param, index) => {
1804
+ for (const dec of decoratorsOf(param)) {
1805
+ if (decoratorName2(dec) === modifierName)
1051
1806
  result.add(index);
1052
1807
  }
1053
1808
  });
1054
1809
  return result;
1055
1810
  }
1056
1811
  function unwrapForwardRef(expr) {
1057
- if (Node.isCallExpression(expr)) {
1058
- const exprText = expr.getExpression().getText();
1812
+ if (ts3.isCallExpression(expr)) {
1813
+ const exprText = nodeText(expr.expression);
1059
1814
  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)) {
1815
+ const arg = expr.arguments[0];
1816
+ if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
1817
+ const body = arg.body;
1818
+ if (body && ts3.isExpression(body)) {
1064
1819
  return unwrapForwardRef(body);
1065
1820
  }
1066
1821
  }
@@ -1068,18 +1823,18 @@ function unwrapForwardRef(expr) {
1068
1823
  }
1069
1824
  return expr;
1070
1825
  }
1071
- function tokenText(expr) {
1826
+ function tokenText(expr, ctx) {
1072
1827
  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();
1828
+ if (ts3.isStringLiteral(unwrapped))
1829
+ return unwrapped.text;
1830
+ if (ts3.isIdentifier(unwrapped)) {
1831
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1832
+ if (decl && ts3.isClassDeclaration(decl))
1833
+ return decl.name?.text ?? unwrapped.text;
1834
+ if (decl && ts3.isVariableDeclaration(decl))
1835
+ return variableName(decl);
1081
1836
  }
1082
- return unwrapped.getText();
1837
+ return nodeText(unwrapped);
1083
1838
  }
1084
1839
  function resolveScope(input, ctx) {
1085
1840
  if (input.explicit)
@@ -1096,98 +1851,181 @@ function resolveScope(input, ctx) {
1096
1851
  }
1097
1852
  function tokenNameOf(expr, ctx) {
1098
1853
  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" };
1854
+ if (ts3.isIdentifier(unwrapped)) {
1855
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1856
+ if (decl && ts3.isClassDeclaration(decl)) {
1857
+ return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
1103
1858
  }
1104
- if (decl && Node.isVariableDeclaration(decl)) {
1105
- const name = decl.getName();
1859
+ if (decl && ts3.isVariableDeclaration(decl)) {
1860
+ const name = variableName(decl);
1106
1861
  return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
1107
1862
  }
1108
- if (ctx.tokensByName.has(expr.getText())) {
1109
- return { name: expr.getText(), kind: "injection-token" };
1863
+ if (ctx.tokensByName.has(unwrapped.text)) {
1864
+ return { name: unwrapped.text, kind: "injection-token" };
1110
1865
  }
1111
1866
  }
1112
- return { name: expr.getText(), kind: "class" };
1867
+ return { name: nodeText(expr), kind: "class" };
1113
1868
  }
1114
- function resolveDeclaration(id) {
1115
- let symbol = id.getSymbol();
1869
+ function resolveDeclaration(id, ctx) {
1870
+ let symbol = ctx.checker.getSymbolAtLocation(id);
1116
1871
  if (!symbol)
1117
1872
  return [];
1118
- let declarations = symbol.getDeclarations();
1873
+ let declarations = symbol.declarations ?? [];
1119
1874
  for (let guard = 0;guard < 4; guard += 1) {
1120
- const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
1875
+ const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
1121
1876
  if (!isAlias)
1122
1877
  break;
1123
- const aliased = symbol.getAliasedSymbol();
1124
- if (!aliased)
1878
+ if (!(symbol.flags & ts3.SymbolFlags.Alias))
1125
1879
  break;
1880
+ const aliased = ctx.checker.getAliasedSymbol(symbol);
1126
1881
  symbol = aliased;
1127
- declarations = aliased.getDeclarations();
1882
+ declarations = aliased.declarations ?? [];
1128
1883
  }
1129
1884
  return declarations;
1130
1885
  }
1131
1886
  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];
1887
+ const decl = resolveDeclaration(id, ctx)[0];
1141
1888
  if (decl)
1142
- return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1889
+ return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
1890
+ return;
1891
+ }
1892
+ function importModuleOf(id, ctx) {
1893
+ const symbol = ctx.checker.getSymbolAtLocation(id);
1894
+ const declarations = symbol?.declarations ?? [];
1895
+ for (const declaration of declarations) {
1896
+ let current = declaration;
1897
+ while (current) {
1898
+ if (ts3.isImportDeclaration(current)) {
1899
+ const moduleSpecifier = current.moduleSpecifier;
1900
+ return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
1901
+ }
1902
+ current = current.parent;
1903
+ }
1904
+ }
1143
1905
  return;
1144
1906
  }
1145
1907
  function findDecorator(cls, name) {
1146
- return cls.getDecorators().find((dec) => decoratorName(dec) === name);
1908
+ return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
1147
1909
  }
1148
- function decoratorName(dec) {
1149
- const expr = dec.getExpression();
1150
- if (Node.isCallExpression(expr)) {
1151
- return expr.getExpression().getText().split(".").pop();
1910
+ function decoratorName2(dec) {
1911
+ const expr = dec.expression;
1912
+ if (ts3.isCallExpression(expr)) {
1913
+ return nodeText(expr.expression).split(".").pop();
1152
1914
  }
1153
- if (Node.isIdentifier(expr))
1154
- return expr.getText();
1915
+ if (ts3.isIdentifier(expr))
1916
+ return expr.text;
1155
1917
  return;
1156
1918
  }
1157
1919
  function decoratorObjectArg(dec) {
1158
- const expr = dec.getExpression();
1159
- if (!Node.isCallExpression(expr))
1920
+ const expr = dec.expression;
1921
+ if (!ts3.isCallExpression(expr))
1160
1922
  return;
1161
- const arg = expr.getArguments()[0];
1162
- return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
1923
+ const arg = expr.arguments[0];
1924
+ return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
1163
1925
  }
1164
1926
  function getProp(obj, name) {
1165
- const prop = obj.getProperty(name);
1166
- if (prop && Node.isPropertyAssignment(prop))
1167
- return prop.getInitializer();
1927
+ const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
1928
+ if (!prop)
1929
+ return;
1930
+ if (ts3.isPropertyAssignment(prop))
1931
+ return prop.initializer;
1932
+ if (ts3.isShorthandPropertyAssignment(prop))
1933
+ return prop.name;
1168
1934
  return;
1169
1935
  }
1936
+ function toCompilerDiagnostic(diagnostic, rootDir) {
1937
+ const file = diagnostic.file;
1938
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
1939
+ return {
1940
+ severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
1941
+ code: `typescript-${diagnostic.code}`,
1942
+ errorCode: `TS${diagnostic.code}`,
1943
+ message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
1944
+ `),
1945
+ file: file ? sourcePath(rootDir, file.fileName) : undefined,
1946
+ line: position ? position.line + 1 : undefined
1947
+ };
1948
+ }
1170
1949
  function stringLiteralProp(obj, name) {
1171
1950
  const expr = getProp(obj, name);
1172
- return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
1951
+ return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
1173
1952
  }
1174
1953
  function arrayProp(obj, name) {
1175
1954
  const expr = getProp(obj, name);
1176
- return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
1955
+ return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
1956
+ }
1957
+ function parseAspectRefs(expression, ctx, owner) {
1958
+ if (!expression)
1959
+ return [];
1960
+ if (!ts3.isArrayLiteralExpression(expression)) {
1961
+ ctx.diagnostics.push({
1962
+ severity: "error",
1963
+ code: "dynamic-aspect-reference",
1964
+ message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
1965
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
1966
+ line: lineOf(expression),
1967
+ suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
1968
+ errorCode: "SC4010",
1969
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1970
+ });
1971
+ return [];
1972
+ }
1973
+ const refs = [];
1974
+ for (const element of expression.elements) {
1975
+ if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
1976
+ ctx.diagnostics.push({
1977
+ severity: "error",
1978
+ code: "dynamic-aspect-reference",
1979
+ message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
1980
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
1981
+ line: lineOf(element),
1982
+ suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
1983
+ errorCode: "SC4010",
1984
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1985
+ });
1986
+ continue;
1987
+ }
1988
+ const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
1989
+ if (!declaration) {
1990
+ ctx.diagnostics.push({
1991
+ severity: "error",
1992
+ code: "invalid-aspect-reference",
1993
+ message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
1994
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
1995
+ line: lineOf(element),
1996
+ suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
1997
+ errorCode: "SC4011",
1998
+ docsUrl: "https://supacloud.dev/errors/SC4011"
1999
+ });
2000
+ continue;
2001
+ }
2002
+ const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
2003
+ if (!name)
2004
+ continue;
2005
+ const declaredFile = declaration.getSourceFile().fileName;
2006
+ const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2007
+ refs.push({
2008
+ name,
2009
+ expression: element.text,
2010
+ importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
2011
+ importModule: projectLocal ? undefined : importModuleOf(element, ctx)
2012
+ });
2013
+ }
2014
+ return refs;
1177
2015
  }
1178
2016
  function booleanProp(obj, name) {
1179
2017
  const expr = getProp(obj, name);
1180
2018
  if (!expr)
1181
2019
  return;
1182
- if (expr.getKind() === SyntaxKind.TrueKeyword)
2020
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
1183
2021
  return true;
1184
- if (expr.getKind() === SyntaxKind.FalseKeyword)
2022
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
1185
2023
  return false;
1186
2024
  return;
1187
2025
  }
1188
2026
  function parseScopeProp(obj) {
1189
2027
  const scope = stringLiteralProp(obj, "scope");
1190
- return scope && SCOPES.includes(scope) ? scope : undefined;
2028
+ return scope && isScope(scope) ? scope : undefined;
1191
2029
  }
1192
2030
  function parseBindingOptions(args, defaultName) {
1193
2031
  let name = defaultName;
@@ -1195,16 +2033,16 @@ function parseBindingOptions(args, defaultName) {
1195
2033
  let defaultValue;
1196
2034
  const first = args[0];
1197
2035
  const second = args[1];
1198
- if (first && Node.isStringLiteral(first)) {
1199
- name = first.getLiteralText();
1200
- } else if (first && Node.isObjectLiteralExpression(first)) {
2036
+ if (first && ts3.isStringLiteral(first)) {
2037
+ name = first.text;
2038
+ } else if (first && ts3.isObjectLiteralExpression(first)) {
1201
2039
  const nameProp = getProp(first, "name");
1202
- if (nameProp && Node.isStringLiteral(nameProp)) {
1203
- name = nameProp.getLiteralText();
2040
+ if (nameProp && ts3.isStringLiteral(nameProp)) {
2041
+ name = nameProp.text;
1204
2042
  }
1205
2043
  const trProp = getProp(first, "transform");
1206
- if (trProp && Node.isStringLiteral(trProp)) {
1207
- const val = trProp.getLiteralText();
2044
+ if (trProp && ts3.isStringLiteral(trProp)) {
2045
+ const val = trProp.text;
1208
2046
  if (val === "number" || val === "boolean" || val === "string") {
1209
2047
  transform = val;
1210
2048
  }
@@ -1214,10 +2052,10 @@ function parseBindingOptions(args, defaultName) {
1214
2052
  defaultValue = parseLiteralValue(defProp);
1215
2053
  }
1216
2054
  }
1217
- if (second && Node.isObjectLiteralExpression(second)) {
2055
+ if (second && ts3.isObjectLiteralExpression(second)) {
1218
2056
  const trProp = getProp(second, "transform");
1219
- if (trProp && Node.isStringLiteral(trProp)) {
1220
- const val = trProp.getLiteralText();
2057
+ if (trProp && ts3.isStringLiteral(trProp)) {
2058
+ const val = trProp.text;
1221
2059
  if (val === "number" || val === "boolean" || val === "string") {
1222
2060
  transform = val;
1223
2061
  }
@@ -1230,28 +2068,28 @@ function parseBindingOptions(args, defaultName) {
1230
2068
  return { name, transform, default: defaultValue };
1231
2069
  }
1232
2070
  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")
2071
+ if (ts3.isStringLiteral(node))
2072
+ return node.text;
2073
+ if (ts3.isNumericLiteral(node))
2074
+ return Number(node.text);
2075
+ if (node.kind === ts3.SyntaxKind.TrueKeyword)
1238
2076
  return true;
1239
- if (node.getKindName() === "FalseKeyword")
2077
+ if (node.kind === ts3.SyntaxKind.FalseKeyword)
1240
2078
  return false;
1241
- if (Node.isArrayLiteralExpression(node)) {
1242
- return node.getElements().map(parseLiteralValue);
2079
+ if (ts3.isArrayLiteralExpression(node)) {
2080
+ return node.elements.map(parseLiteralValue);
1243
2081
  }
1244
- if (Node.isObjectLiteralExpression(node)) {
2082
+ if (ts3.isObjectLiteralExpression(node)) {
1245
2083
  return parseObjectLiteralValues(node);
1246
2084
  }
1247
2085
  return;
1248
2086
  }
1249
2087
  function parseObjectLiteralValues(obj) {
1250
2088
  const result = {};
1251
- for (const prop of obj.getProperties()) {
1252
- if (Node.isPropertyAssignment(prop)) {
1253
- const name = prop.getName();
1254
- const init = prop.getInitializer();
2089
+ for (const prop of obj.properties) {
2090
+ if (ts3.isPropertyAssignment(prop)) {
2091
+ const name = propertyName(prop.name);
2092
+ const init = prop.initializer;
1255
2093
  if (init) {
1256
2094
  result[name] = parseLiteralValue(init);
1257
2095
  }
@@ -1269,7 +2107,8 @@ function warn(ctx, code, message, file, line) {
1269
2107
  ctx.diagnostics.push({ severity: "warn", code, message, file, line });
1270
2108
  }
1271
2109
  // src/generate.ts
1272
- import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
2110
+ import { createHash as createHash4 } from "node:crypto";
2111
+ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
1273
2112
  import { join as join2 } from "node:path";
1274
2113
 
1275
2114
  // src/util.ts
@@ -1348,6 +2187,7 @@ var INTERFACES = `export interface CompiledRoute {
1348
2187
  queryDefaults?: Record<string, unknown>;
1349
2188
  title?: string;
1350
2189
  data?: Record<string, unknown>;
2190
+ aspects?: CompiledAspect[];
1351
2191
  invoker?: (
1352
2192
  controller: unknown,
1353
2193
  request: {
@@ -1368,8 +2208,33 @@ export interface CompiledCommand {
1368
2208
  audit?: string;
1369
2209
  idempotency: "required" | "none";
1370
2210
  standalone?: boolean;
2211
+ aspects?: CompiledAspect[];
2212
+ }
2213
+
2214
+ export interface CompiledJob {
2215
+ className: string;
2216
+ name: string;
2217
+ serviceKey: string;
2218
+ scope: "application" | "request" | "job";
2219
+ aspects?: CompiledAspect[];
2220
+ }
2221
+
2222
+ export interface CompiledAspectContext {
2223
+ kind: "route" | "command" | "job";
2224
+ name: string;
2225
+ input: unknown;
2226
+ request?: Request;
2227
+ requestContext?: unknown;
2228
+ scope?: Record<string, unknown>;
2229
+ services?: Record<string, unknown>;
2230
+ metadata?: unknown;
1371
2231
  }
1372
2232
 
2233
+ export type CompiledAspect = (
2234
+ context: CompiledAspectContext,
2235
+ next: () => unknown | Promise<unknown>,
2236
+ ) => unknown | Promise<unknown>;
2237
+
1373
2238
  export interface CompiledController {
1374
2239
  path: string;
1375
2240
  serviceKey: string;
@@ -1388,13 +2253,62 @@ export interface CompiledModule {
1388
2253
  ctx: unknown,
1389
2254
  imported?: Record<string, Record<string, unknown>>,
1390
2255
  ): Record<string, unknown>;
2256
+ destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
1391
2257
  createJobScope?(
1392
2258
  services: Record<string, unknown>,
1393
2259
  ctx: unknown,
1394
2260
  imported?: Record<string, Record<string, unknown>>,
1395
2261
  ): Record<string, unknown>;
2262
+ destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
1396
2263
  controllers: CompiledController[];
1397
2264
  commands: CompiledCommand[];
2265
+ jobs: CompiledJob[];
2266
+ aspects?: CompiledAspect[];
2267
+ }`;
2268
+ var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
2269
+ return typeof value === "object" && value !== null;
2270
+ }
2271
+
2272
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2273
+ return typeof value === "function";
2274
+ }
2275
+
2276
+ function resolveFactoryValue(value: unknown): unknown {
2277
+ if (!isRecord(value) || !isFunction(value.factory)) return undefined;
2278
+ return value.factory();
2279
+ }
2280
+
2281
+ const scopeDestructions = new WeakMap<object, Promise<void>>();
2282
+
2283
+ function destroyScopeInstances(
2284
+ scope: Record<string, unknown>,
2285
+ plan: readonly { key: string; index?: number }[],
2286
+ ): Promise<void> {
2287
+ const pending = scopeDestructions.get(scope);
2288
+ if (pending) return pending;
2289
+ const destruction = Promise.resolve().then(async () => {
2290
+ const errors: unknown[] = [];
2291
+ const seen = new Set<unknown>();
2292
+ for (const entry of [...plan].reverse()) {
2293
+ const value = scope[entry.key];
2294
+ const instance = entry.index === undefined ? value
2295
+ : Array.isArray(value) ? value[entry.index] : undefined;
2296
+ if (seen.has(instance)) continue;
2297
+ seen.add(instance);
2298
+ try {
2299
+ if (isRecord(instance) && isFunction(instance.onDestroy)) {
2300
+ await instance.onDestroy();
2301
+ } else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
2302
+ await instance.ngOnDestroy();
2303
+ }
2304
+ } catch (error) {
2305
+ errors.push(error);
2306
+ }
2307
+ }
2308
+ if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
2309
+ });
2310
+ scopeDestructions.set(scope, destruction);
2311
+ return destruction;
1398
2312
  }`;
1399
2313
  function renderApplication(graph, options) {
1400
2314
  let modules = topoSortModules(graph.modules);
@@ -1412,6 +2326,8 @@ function renderApplication(graph, options) {
1412
2326
  referencedTokens.add(d);
1413
2327
  for (const d of ctrl.skipSelfDeps ?? [])
1414
2328
  referencedTokens.add(d);
2329
+ for (const d of ctrl.hostDeps ?? [])
2330
+ referencedTokens.add(d);
1415
2331
  }
1416
2332
  for (const p of mod.providers) {
1417
2333
  for (const d of p.deps ?? [])
@@ -1422,6 +2338,8 @@ function renderApplication(graph, options) {
1422
2338
  referencedTokens.add(d);
1423
2339
  for (const d of p.skipSelfDeps ?? [])
1424
2340
  referencedTokens.add(d);
2341
+ for (const d of p.hostDeps ?? [])
2342
+ referencedTokens.add(d);
1425
2343
  if (p.useExisting)
1426
2344
  referencedTokens.add(p.useExisting);
1427
2345
  }
@@ -1446,6 +2364,8 @@ function renderApplication(graph, options) {
1446
2364
  ...imports.size > 0 ? [""] : [],
1447
2365
  INTERFACES,
1448
2366
  "",
2367
+ TYPE_GUARDS,
2368
+ "",
1449
2369
  "export function createCompiledModules(): CompiledModule[] {",
1450
2370
  " return [",
1451
2371
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
@@ -1453,29 +2373,33 @@ function renderApplication(graph, options) {
1453
2373
  "}",
1454
2374
  "",
1455
2375
  "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();',
2376
+ ' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
2377
+ " for (const group of initializers) {",
2378
+ " if (Array.isArray(group)) {",
2379
+ " for (const init of group) {",
2380
+ " if (isFunction(init)) await init();",
2381
+ " }",
2382
+ " } else if (isFunction(group)) {",
2383
+ " await group();",
1460
2384
  " }",
1461
- ' } else if (typeof initializers === "function") {',
1462
- " await (initializers as () => unknown)();",
1463
2385
  " }",
1464
2386
  "}",
1465
2387
  "",
1466
2388
  "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") {',
2389
+ ' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
2390
+ " if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
1469
2391
  " await destroyRef.destroy();",
1470
- " } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
2392
+ " } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
1471
2393
  " for (const teardown of [...destroyRef._teardowns].reverse()) {",
1472
- ' if (typeof teardown === "function") await teardown();',
2394
+ " if (isFunction(teardown)) await teardown();",
1473
2395
  " }",
1474
2396
  " }",
1475
2397
  " const instances = Object.values(services);",
1476
2398
  " for (const inst of instances.reverse()) {",
1477
- ' if (inst && typeof (inst as any).onDestroy === "function") {',
1478
- " await (inst as any).onDestroy();",
2399
+ " if (isRecord(inst) && isFunction(inst.onDestroy)) {",
2400
+ " await inst.onDestroy();",
2401
+ " } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
2402
+ " await inst.ngOnDestroy();",
1479
2403
  " }",
1480
2404
  " }",
1481
2405
  "}",
@@ -1504,21 +2428,39 @@ async function generateApplication(graph, options) {
1504
2428
  await mkdir(options.outDir, { recursive: true });
1505
2429
  const applicationPath = join2(options.outDir, "application.ts");
1506
2430
  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];
2431
+ const written = [];
2432
+ if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
2433
+ written.push(applicationPath);
2434
+ }
2435
+ if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
2436
+ written.push(manifestPath);
2437
+ }
1510
2438
  if (rendered.clientCode) {
1511
2439
  const clientPath = join2(options.outDir, "client.ts");
1512
- await writeFileAtomic(clientPath, rendered.clientCode);
1513
- written.push(clientPath);
2440
+ if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
2441
+ written.push(clientPath);
2442
+ }
1514
2443
  }
1515
2444
  if (rendered.permissionsCode) {
1516
2445
  const permissionsPath = join2(options.outDir, "permissions.ts");
1517
- await writeFileAtomic(permissionsPath, rendered.permissionsCode);
1518
- written.push(permissionsPath);
2446
+ if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
2447
+ written.push(permissionsPath);
2448
+ }
1519
2449
  }
1520
2450
  return written;
1521
2451
  }
2452
+ async function writeFileIfChanged(path, content, hashes) {
2453
+ const hash = createHash4("sha1").update(content).digest("hex");
2454
+ if (hashes?.get(path) === hash) {
2455
+ try {
2456
+ await access(path);
2457
+ return false;
2458
+ } catch {}
2459
+ }
2460
+ await writeFileAtomic(path, content);
2461
+ hashes?.set(path, hash);
2462
+ return true;
2463
+ }
1522
2464
  async function writeFileAtomic(path, content) {
1523
2465
  const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1524
2466
  try {
@@ -1569,11 +2511,13 @@ class ImportManager {
1569
2511
  get size() {
1570
2512
  return this.entries.size;
1571
2513
  }
1572
- add(exported, importPath) {
1573
- if (!importPath)
2514
+ add(exported, importPath, importModule) {
2515
+ const path = importModule ?? importPath;
2516
+ const packageImport = importModule !== undefined;
2517
+ if (!path)
1574
2518
  return exported;
1575
2519
  for (const [local2, entry] of this.entries) {
1576
- if (entry.path === importPath && entry.exported === exported)
2520
+ if (entry.path === path && entry.exported === exported && entry.package === packageImport)
1577
2521
  return local2;
1578
2522
  }
1579
2523
  let local = exported;
@@ -1582,18 +2526,18 @@ class ImportManager {
1582
2526
  local = `${exported}${counter}`;
1583
2527
  counter += 1;
1584
2528
  }
1585
- this.entries.set(local, { path: importPath, exported });
2529
+ this.entries.set(local, { path, exported, package: packageImport });
1586
2530
  return local;
1587
2531
  }
1588
2532
  render(rootDir, outDir) {
1589
2533
  const byPath = new Map;
1590
2534
  for (const [local, entry] of this.entries) {
1591
- const list = byPath.get(entry.path) ?? [];
2535
+ const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
2536
+ const list = byPath.get(spec) ?? [];
1592
2537
  list.push({ exported: entry.exported, local });
1593
- byPath.set(entry.path, list);
2538
+ byPath.set(spec, list);
1594
2539
  }
1595
- return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
1596
- const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
2540
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
1597
2541
  const names = symbols.sort((a, b) => a.exported.localeCompare(b.exported)).map((s) => s.local === s.exported ? s.exported : `${s.exported} as ${s.local}`).join(", ");
1598
2542
  return `import { ${names} } from "${spec}";`;
1599
2543
  });
@@ -1615,14 +2559,19 @@ class ModuleGenerator {
1615
2559
  this.module = module;
1616
2560
  this.imports = imports;
1617
2561
  this.pascal = pascalName(module.name);
2562
+ if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
2563
+ imports.add("runInInjectionContext", undefined, "@supacloud/app");
2564
+ }
1618
2565
  }
1619
2566
  renderFactories() {
1620
2567
  const sections = [this.renderServicesFactory()];
1621
2568
  if (this.hasFactoryContent("request")) {
1622
2569
  sections.push(this.renderScopeFactory("request"));
2570
+ sections.push(this.renderScopeDestroyer("request"));
1623
2571
  }
1624
2572
  if (this.hasFactoryContent("job")) {
1625
2573
  sections.push(this.renderScopeFactory("job"));
2574
+ sections.push(this.renderScopeDestroyer("job"));
1626
2575
  }
1627
2576
  return sections;
1628
2577
  }
@@ -1634,12 +2583,18 @@ class ModuleGenerator {
1634
2583
  ];
1635
2584
  if (this.hasFactoryContent("request")) {
1636
2585
  lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
2586
+ lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
1637
2587
  }
1638
2588
  if (this.hasFactoryContent("job")) {
1639
2589
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
2590
+ lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
1640
2591
  }
1641
2592
  lines.push(` controllers: ${this.renderControllers()},`);
1642
- lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
2593
+ lines.push(` commands: ${this.renderCommands()},`);
2594
+ lines.push(` jobs: ${this.renderJobs()},`);
2595
+ if (this.module.aspects && this.module.aspects.length > 0) {
2596
+ lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
2597
+ }
1643
2598
  lines.push(`}`);
1644
2599
  return lines.join(`
1645
2600
  `);
@@ -1702,6 +2657,9 @@ class ModuleGenerator {
1702
2657
  if (route.data && Object.keys(route.data).length > 0) {
1703
2658
  fields.push(`data: ${JSON.stringify(route.data)}`);
1704
2659
  }
2660
+ if (route.aspects && route.aspects.length > 0) {
2661
+ fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2662
+ }
1705
2663
  const invokerArgs = (route.handlerParams ?? []).map((hp) => {
1706
2664
  if (hp.kind === "param") {
1707
2665
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
@@ -1740,7 +2698,7 @@ class ModuleGenerator {
1740
2698
  return "undefined";
1741
2699
  });
1742
2700
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
1743
- fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
2701
+ fields.push(`invoker: async (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (typeof handler !== "function") throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return await Reflect.apply(handler, ctrl, [${callArgs}]); }`);
1744
2702
  return `{ ${fields.join(", ")} }`;
1745
2703
  });
1746
2704
  return [
@@ -1757,6 +2715,32 @@ class ModuleGenerator {
1757
2715
  ${indent(item, 2)}`).join(",")}
1758
2716
  ]`;
1759
2717
  }
2718
+ renderCommands() {
2719
+ if (this.module.commands.length === 0)
2720
+ return "[]";
2721
+ return `[${this.module.commands.map((command) => {
2722
+ const fields = [
2723
+ `className: ${JSON.stringify(command.className)}`,
2724
+ `name: ${JSON.stringify(command.name)}`,
2725
+ `permission: ${JSON.stringify(command.permission ?? "")}`,
2726
+ `transaction: ${JSON.stringify(command.transaction)}`,
2727
+ ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2728
+ `idempotency: ${JSON.stringify(command.idempotency)}`,
2729
+ ...command.standalone ? ["standalone: true"] : [],
2730
+ ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2731
+ ];
2732
+ return `{ ${fields.join(", ")} }`;
2733
+ }).join(", ")}]`;
2734
+ }
2735
+ renderJobs() {
2736
+ const jobs = this.module.jobs ?? [];
2737
+ if (jobs.length === 0)
2738
+ return "[]";
2739
+ return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(camelName(job.className))}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
2740
+ }
2741
+ renderAspects(aspects) {
2742
+ return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
2743
+ }
1760
2744
  renderServicesFactory() {
1761
2745
  return [
1762
2746
  `function create${this.pascal}Services(`,
@@ -1779,6 +2763,30 @@ ${indent(item, 2)}`).join(",")}
1779
2763
  indent(this.renderFactoryBody(kind), 2),
1780
2764
  `}`
1781
2765
  ].join(`
2766
+ `);
2767
+ }
2768
+ renderScopeDestroyer(kind) {
2769
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
2770
+ const plan = [];
2771
+ const multiIndices = new Map;
2772
+ for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
2773
+ const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
2774
+ if (index !== undefined)
2775
+ multiIndices.set(provider.token, index + 1);
2776
+ if (provider.kind !== "existing" && provider.hasOnDestroy) {
2777
+ plan.push({ key: camelName(provider.token), index });
2778
+ }
2779
+ }
2780
+ for (const controller of this.module.controllers) {
2781
+ if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
2782
+ plan.push({ key: camelName(controller.className) });
2783
+ }
2784
+ }
2785
+ return [
2786
+ `async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
2787
+ ` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
2788
+ `}`
2789
+ ].join(`
1782
2790
  `);
1783
2791
  }
1784
2792
  renderFactoryBody(kind) {
@@ -1819,39 +2827,76 @@ ${indent(item, 2)}`).join(",")}
1819
2827
  const key = camelName(provider.token);
1820
2828
  switch (provider.kind) {
1821
2829
  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(", ");
2830
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
2831
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1824
2832
  const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
1825
- return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
2833
+ return {
2834
+ constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
2835
+ key,
2836
+ expr: local
2837
+ };
1826
2838
  }
1827
2839
  case "value": {
1828
- const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
2840
+ const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
1829
2841
  const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1830
2842
  return { constLine: `const ${local} = ${expr};`, key, expr: local };
1831
2843
  }
1832
2844
  case "factory": {
1833
2845
  if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
1834
- const tokenIdent = this.imports.add(provider.token, provider.importPath);
2846
+ const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
1835
2847
  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;`;
2848
+ const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
1837
2849
  return { constLine, key, expr: local2 };
1838
2850
  }
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(", ");
2851
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2852
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1841
2853
  const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
1842
2854
  return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
1843
2855
  }
1844
2856
  case "existing": {
1845
- return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
2857
+ return {
2858
+ key,
2859
+ expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
2860
+ };
1846
2861
  }
1847
2862
  }
1848
2863
  }
1849
2864
  emitController(controller, kind) {
1850
2865
  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(", ");
2866
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
1852
2867
  const key = camelName(controller.className);
1853
2868
  const local = this.localVar(controller.className, kind);
1854
- return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
2869
+ return {
2870
+ constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
2871
+ key,
2872
+ expr: local
2873
+ };
2874
+ }
2875
+ instantiate(className, args, kind, functionalInjects) {
2876
+ if (!functionalInjects || functionalInjects.length === 0) {
2877
+ return `new ${className}(${args})`;
2878
+ }
2879
+ const clauses = functionalInjects.map((entry) => {
2880
+ const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
2881
+ const value = this.depExpr(entry.token, kind, {
2882
+ optional: entry.optional,
2883
+ self: entry.self,
2884
+ skipSelf: entry.skipSelf,
2885
+ host: entry.host
2886
+ });
2887
+ return `if (token === ${token}) return ${value} as T;`;
2888
+ });
2889
+ const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
2890
+ const injector = [
2891
+ `{`,
2892
+ `get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
2893
+ ...clauses,
2894
+ missing,
2895
+ `},`,
2896
+ `}`
2897
+ ].join(`
2898
+ `);
2899
+ return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
1855
2900
  }
1856
2901
  localVar(token, kind) {
1857
2902
  const locals = this.locals[kind];
@@ -1868,23 +2913,34 @@ ${indent(item, 2)}`).join(",")}
1868
2913
  locals.set(token, local);
1869
2914
  return local;
1870
2915
  }
1871
- depExpr(token, kind, isOptional = false) {
2916
+ depOptions(node, token) {
2917
+ return {
2918
+ optional: node.optionalDeps?.includes(token) ?? false,
2919
+ self: node.selfDeps?.includes(token) ?? false,
2920
+ skipSelf: node.skipSelfDeps?.includes(token) ?? false,
2921
+ host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
2922
+ };
2923
+ }
2924
+ depExpr(token, kind, options = {}) {
2925
+ const isOptional = options.optional ?? false;
2926
+ const isSelf = options.self ?? false;
2927
+ const isSkipSelf = options.skipSelf ?? false;
1872
2928
  if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
1873
2929
  return "ctx";
1874
2930
  if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
1875
2931
  return "ctx";
1876
2932
  const own = this.module.providers.find((p) => p.token === token);
1877
- if (own) {
2933
+ const ownIsLocal = own && factoryOfScope(own.scope) === kind;
2934
+ if (own && ownIsLocal && !isSkipSelf) {
1878
2935
  if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
1879
2936
  return this.locals[kind].get(token) ?? camelName(token);
1880
2937
  }
1881
- if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
1882
- return this.depExpr(own.useExisting ?? token, kind, isOptional);
1883
- }
1884
- if (kind === "services") {
1885
- return `services.${camelName(token)}`;
2938
+ if (own.kind === "existing") {
2939
+ return this.depExpr(own.useExisting ?? token, kind, options);
1886
2940
  }
1887
- return `services.${camelName(token)}`;
2941
+ }
2942
+ if (isSelf) {
2943
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1888
2944
  }
1889
2945
  for (const importName of this.module.imports) {
1890
2946
  const imported = this.graph.modules.find((m) => m.name === importName);
@@ -1903,6 +2959,8 @@ ${indent(item, 2)}`).join(",")}
1903
2959
  if (isOptional && !this.graph.externalTokens.includes(token)) {
1904
2960
  return "undefined";
1905
2961
  }
2962
+ if (isSelf)
2963
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1906
2964
  if (kind === "services")
1907
2965
  return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
1908
2966
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
@@ -2360,12 +3418,16 @@ var COMPILER_DIAGNOSTIC_CODES = {
2360
3418
  "conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
2361
3419
  "missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
2362
3420
  "missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
3421
+ "provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
3422
+ "unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
2363
3423
  "command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
2364
3424
  "duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
2365
3425
  "route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
2366
3426
  "command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
2367
3427
  "route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
2368
3428
  "command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
3429
+ "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3430
+ "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
2369
3431
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
2370
3432
  };
2371
3433
  function validateGraph(graph, options = false) {
@@ -2399,10 +3461,14 @@ function validateGraph(graph, options = false) {
2399
3461
  }
2400
3462
  }
2401
3463
  }
2402
- function resolveDep(module, token) {
2403
- const own = module.providers.find((p) => p.token === token);
2404
- if (own)
2405
- return { module, provider: own };
3464
+ function resolveDep(module, token, flags = {}) {
3465
+ if (!flags.skipSelf) {
3466
+ const own = module.providers.find((p) => p.token === token);
3467
+ if (own)
3468
+ return { module, provider: own };
3469
+ }
3470
+ if (flags.self)
3471
+ return;
2406
3472
  for (const importName of module.imports) {
2407
3473
  const imported = graph.modules.find((m) => m.name === importName);
2408
3474
  if (!imported || !imported.exports.includes(token))
@@ -2600,7 +3666,7 @@ function validateGraph(graph, options = false) {
2600
3666
  }
2601
3667
  if (controller.selfDeps && controller.selfDeps.length > 0) {
2602
3668
  for (const dep of controller.selfDeps) {
2603
- const own = module.providers.find((p) => p.token === dep);
3669
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2604
3670
  if (!own) {
2605
3671
  error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2606
3672
  }
@@ -2608,7 +3674,7 @@ function validateGraph(graph, options = false) {
2608
3674
  }
2609
3675
  if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
2610
3676
  for (const dep of controller.skipSelfDeps) {
2611
- const own = module.providers.find((p) => p.token === dep);
3677
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2612
3678
  if (own) {
2613
3679
  error("skip-self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, controller.file, undefined, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2614
3680
  }
@@ -2683,7 +3749,7 @@ function validateGraph(graph, options = false) {
2683
3749
  for (const provider of module.providers) {
2684
3750
  if (provider.selfDeps && provider.selfDeps.length > 0) {
2685
3751
  for (const dep of provider.selfDeps) {
2686
- const own = module.providers.find((p) => p.token === dep);
3752
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2687
3753
  if (!own) {
2688
3754
  error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2689
3755
  }
@@ -2691,7 +3757,7 @@ function validateGraph(graph, options = false) {
2691
3757
  }
2692
3758
  if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
2693
3759
  for (const dep of provider.skipSelfDeps) {
2694
- const own = module.providers.find((p) => p.token === dep);
3760
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2695
3761
  if (own) {
2696
3762
  error("skip-self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, provider.file, provider.line, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2697
3763
  }
@@ -2699,7 +3765,10 @@ function validateGraph(graph, options = false) {
2699
3765
  }
2700
3766
  for (const dep of provider.deps) {
2701
3767
  const isOptional = provider.optionalDeps?.includes(dep);
2702
- const resolved = resolveDep(module, dep);
3768
+ const resolved = resolveDep(module, dep, {
3769
+ self: provider.selfDeps?.includes(dep),
3770
+ skipSelf: provider.skipSelfDeps?.includes(dep)
3771
+ });
2703
3772
  if (!resolved) {
2704
3773
  if (isOptional) {
2705
3774
  continue;
@@ -2707,6 +3776,8 @@ function validateGraph(graph, options = false) {
2707
3776
  if (!graph.externalTokens.includes(dep)) {
2708
3777
  if (globalProviders.has(dep)) {
2709
3778
  const owner = globalProviders.get(dep);
3779
+ if (!owner)
3780
+ continue;
2710
3781
  error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line, `Import module '${owner.module.name}' in '${module.name}', add '${dep}' to '${owner.module.name}' exports, or mark @Injectable({ providedIn: 'root' }).`);
2711
3782
  } else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
2712
3783
  error("missing-token-factory", `InjectionToken '${dep}' referenced by provider '${provider.token}' has no provider in module '${module.name}' and no default factory function.`, provider.file, provider.line, `Provide '${dep}' in @Module({ providers: [...] }) or declare it with new InjectionToken('${dep}', { factory: () => ... }).`);
@@ -2767,7 +3838,8 @@ function validateGraph(graph, options = false) {
2767
3838
  }
2768
3839
  }
2769
3840
  if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
2770
- const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
3841
+ const allowedTags = rule.onlyDependOnLibsWithTags;
3842
+ const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
2771
3843
  if (!hasAllowed && targetTags.length > 0) {
2772
3844
  error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
2773
3845
  }
@@ -2789,6 +3861,8 @@ function validateGraph(graph, options = false) {
2789
3861
  referencedTokens.add(d);
2790
3862
  for (const d of ctrl.skipSelfDeps ?? [])
2791
3863
  referencedTokens.add(d);
3864
+ for (const d of ctrl.hostDeps ?? [])
3865
+ referencedTokens.add(d);
2792
3866
  }
2793
3867
  for (const p of mod.providers) {
2794
3868
  for (const d of p.deps ?? [])
@@ -2799,6 +3873,8 @@ function validateGraph(graph, options = false) {
2799
3873
  referencedTokens.add(d);
2800
3874
  for (const d of p.skipSelfDeps ?? [])
2801
3875
  referencedTokens.add(d);
3876
+ for (const d of p.hostDeps ?? [])
3877
+ referencedTokens.add(d);
2802
3878
  if (p.useExisting)
2803
3879
  referencedTokens.add(p.useExisting);
2804
3880
  }
@@ -2921,7 +3997,10 @@ function detectCycles(graph, resolveDep) {
2921
3997
  state.set(id, "visiting");
2922
3998
  stack.push(ref);
2923
3999
  for (const dep of ref.provider.deps) {
2924
- const resolved = resolveDep(ref.module, dep);
4000
+ const resolved = resolveDep(ref.module, dep, {
4001
+ self: ref.provider.selfDeps?.includes(dep),
4002
+ skipSelf: ref.provider.skipSelfDeps?.includes(dep)
4003
+ });
2925
4004
  if (resolved)
2926
4005
  visit(resolved);
2927
4006
  }
@@ -3034,6 +4113,8 @@ function detectOrphanModules(graph) {
3034
4113
  }
3035
4114
  while (queue.length > 0) {
3036
4115
  const current = queue.shift();
4116
+ if (!current)
4117
+ continue;
3037
4118
  const mod = moduleMap.get(current);
3038
4119
  if (!mod)
3039
4120
  continue;
@@ -3062,10 +4143,225 @@ function detectOrphanModules(graph) {
3062
4143
  }
3063
4144
 
3064
4145
  // src/compile.ts
3065
- import { existsSync as existsSync2, readFileSync } from "node:fs";
3066
- import { join as join3 } from "node:path";
4146
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4147
+ import { join as join4 } from "node:path";
4148
+
4149
+ // src/type-safety.ts
4150
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4151
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4152
+ import * as ts4 from "@typescript/typescript6";
4153
+ var DEFAULT_EXCLUDES = [
4154
+ "**/*.test.ts",
4155
+ "**/*.spec.ts",
4156
+ "**/test/**",
4157
+ "**/tests/**",
4158
+ "**/__tests__/**",
4159
+ "**/fixtures/**",
4160
+ "**/generated/**",
4161
+ "**/dist/**",
4162
+ "**/*.d.ts"
4163
+ ];
4164
+ var DIAGNOSTIC_META = {
4165
+ "generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
4166
+ "source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
4167
+ "source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
4168
+ "source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
4169
+ "source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
4170
+ };
4171
+ function scanGeneratedArtifacts(artifacts, strict = true) {
4172
+ const diagnostics = [];
4173
+ for (const [file, content] of Object.entries(artifacts)) {
4174
+ if (content === undefined)
4175
+ continue;
4176
+ const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
4177
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4178
+ diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
4179
+ }
4180
+ }
4181
+ return diagnostics;
4182
+ }
4183
+ function scanProductionSource(options) {
4184
+ const rootDir = resolve2(options.rootDir);
4185
+ const configPath = join3(rootDir, "tsconfig.json");
4186
+ const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
4187
+ options: {
4188
+ strict: true,
4189
+ skipLibCheck: true,
4190
+ target: ts4.ScriptTarget.ES2022,
4191
+ module: ts4.ModuleKind.ESNext
4192
+ },
4193
+ errors: []
4194
+ };
4195
+ const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
4196
+ const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
4197
+ const compilerOptions = { ...projectConfig.options, noEmit: true };
4198
+ const host = ts4.createCompilerHost(compilerOptions);
4199
+ host.getCurrentDirectory = () => rootDir;
4200
+ const program = ts4.createProgram(rootNames, compilerOptions, host);
4201
+ const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
4202
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
4203
+ const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
4204
+ const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
4205
+ severity: "error",
4206
+ code: "source-config",
4207
+ message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
4208
+ `),
4209
+ file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
4210
+ line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
4211
+ errorCode: `TS${diagnostic.code}`
4212
+ }));
4213
+ const checker = program.getTypeChecker();
4214
+ for (const sourceFile of sourceFiles) {
4215
+ scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
4216
+ }
4217
+ return diagnostics;
4218
+ }
4219
+ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
4220
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4221
+ diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
4222
+ }
4223
+ for (const node of descendants(sourceFile)) {
4224
+ if (ts4.isAsExpression(node)) {
4225
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4226
+ continue;
4227
+ const assertedType = node.type.getText(sourceFile);
4228
+ if (assertedType === "const")
4229
+ continue;
4230
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4231
+ } else if (ts4.isTypeAssertionExpression(node)) {
4232
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4233
+ continue;
4234
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4235
+ } else if (ts4.isNonNullExpression(node)) {
4236
+ diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
4237
+ }
4238
+ }
4239
+ for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
4240
+ const initializer = declaration.initializer;
4241
+ if (!initializer || declaration.type)
4242
+ continue;
4243
+ const declarationType = checker.getTypeAtLocation(declaration.name);
4244
+ const initializerType = checker.getTypeAtLocation(initializer);
4245
+ for (const name of bindingNames(declaration.name)) {
4246
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4247
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
4248
+ }
4249
+ }
4250
+ if (isAnyType(declarationType))
4251
+ continue;
4252
+ if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
4253
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
4254
+ }
4255
+ if (ts4.isObjectLiteralExpression(initializer) && isConstDeclaration(declaration) && initializer.getText(sourceFile).length > 0 && initializer.properties.some((property) => ts4.isPropertyAssignment(property) && property.initializer !== undefined && !ts4.isAsExpression(property.initializer) && isLiteralExpression(property.initializer))) {
4256
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
4257
+ }
4258
+ }
4259
+ for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
4260
+ if (parameter.type)
4261
+ continue;
4262
+ for (const name of bindingNames(parameter.name)) {
4263
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4264
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
4265
+ }
4266
+ }
4267
+ }
4268
+ }
4269
+ function readProjectConfig2(configPath) {
4270
+ const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
4271
+ if (config.error)
4272
+ return { options: {}, errors: [config.error] };
4273
+ const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
4274
+ return { options: parsed.options, errors: parsed.errors };
4275
+ }
4276
+ function isProductionSource(rootDir, sourceFile, excludes, outDir) {
4277
+ const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
4278
+ if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
4279
+ return false;
4280
+ if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
4281
+ return false;
4282
+ return !excludes.some((pattern) => globMatches(relativePath, pattern));
4283
+ }
4284
+ function isProductionSourcePath(rootDir, filePath, excludes) {
4285
+ const relativePath = normalizeRelative(rootDir, filePath);
4286
+ return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
4287
+ }
4288
+ function globMatches(value, pattern) {
4289
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
4290
+ return new RegExp(`^${escaped}$`).test(value);
4291
+ }
4292
+ function bindingNames(name) {
4293
+ if (ts4.isIdentifier(name))
4294
+ return [name];
4295
+ return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
4296
+ }
4297
+ function isLiteralExpression(node) {
4298
+ if (!node)
4299
+ return false;
4300
+ return [
4301
+ ts4.SyntaxKind.StringLiteral,
4302
+ ts4.SyntaxKind.NumericLiteral,
4303
+ ts4.SyntaxKind.TrueKeyword,
4304
+ ts4.SyntaxKind.FalseKeyword
4305
+ ].includes(node.kind);
4306
+ }
4307
+ function isLiteralSyntax(node) {
4308
+ return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
4309
+ }
4310
+ function isLiteralType(type) {
4311
+ return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
4312
+ }
4313
+ function isAnyType(type) {
4314
+ return (type.flags & ts4.TypeFlags.Any) !== 0;
4315
+ }
4316
+ function isLetDeclaration(declaration) {
4317
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4318
+ }
4319
+ function isConstDeclaration(declaration) {
4320
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
4321
+ }
4322
+ function descendants(root) {
4323
+ const result = [];
4324
+ const visit = (node) => {
4325
+ result.push(node);
4326
+ ts4.forEachChild(node, visit);
4327
+ };
4328
+ ts4.forEachChild(root, visit);
4329
+ return result;
4330
+ }
4331
+ function descendantsOfKind2(root, predicate) {
4332
+ const result = [];
4333
+ const visit = (node) => {
4334
+ if (predicate(node))
4335
+ result.push(node);
4336
+ ts4.forEachChild(node, visit);
4337
+ };
4338
+ ts4.forEachChild(root, visit);
4339
+ return result;
4340
+ }
4341
+ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
4342
+ const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
4343
+ const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
4344
+ const meta = DIAGNOSTIC_META[code];
4345
+ return {
4346
+ severity: strict ? "error" : "warn",
4347
+ code,
4348
+ message,
4349
+ file,
4350
+ line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
4351
+ errorCode: meta.errorCode,
4352
+ docsUrl: meta.docsUrl
4353
+ };
4354
+ }
4355
+ function normalizeRelative(rootDir, filePath) {
4356
+ return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4357
+ }
4358
+ function isAnyKeyword(node) {
4359
+ return node.kind === ts4.SyntaxKind.AnyKeyword;
4360
+ }
4361
+
4362
+ // src/compile.ts
3067
4363
  async function compileProject(options) {
3068
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4364
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3069
4365
  const diagnostics = [
3070
4366
  ...graph.diagnostics ?? [],
3071
4367
  ...validateGraph(graph, {
@@ -3084,14 +4380,40 @@ async function compileProject(options) {
3084
4380
  diagnostic.severity = "error";
3085
4381
  }
3086
4382
  }
3087
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
3088
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
4383
+ const typeSafety = resolveTypeSafety(options);
4384
+ const rendered = renderApplication(graph, {
3089
4385
  rootDir: options.rootDir,
3090
4386
  outDir: options.outDir,
3091
4387
  generateClient: options.generateClient,
3092
4388
  generatePermissions: options.generatePermissions,
3093
4389
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3094
- }) : [];
4390
+ });
4391
+ if (typeSafety.scanProductionSource) {
4392
+ diagnostics.push(...scanProductionSource({
4393
+ rootDir: options.rootDir,
4394
+ include: options.include,
4395
+ outDir: options.outDir,
4396
+ strict: options.strict,
4397
+ ...typeSafety
4398
+ }));
4399
+ }
4400
+ if (typeSafety.noAnyInGenerated) {
4401
+ diagnostics.push(...scanGeneratedArtifacts({
4402
+ "application.ts": rendered.applicationCode,
4403
+ "client.ts": rendered.clientCode,
4404
+ "permissions.ts": rendered.permissionsCode
4405
+ }, options.strict ?? false));
4406
+ }
4407
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
4408
+ const generatedOptions = {
4409
+ rootDir: options.rootDir,
4410
+ outDir: options.outDir,
4411
+ generateClient: options.generateClient,
4412
+ generatePermissions: options.generatePermissions,
4413
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4414
+ artifactHashes: options.cache?.generatedHashes
4415
+ };
4416
+ const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
3095
4417
  const stats = graph.cacheStats ? {
3096
4418
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
3097
4419
  changedFiles: [],
@@ -3102,7 +4424,7 @@ async function compileProject(options) {
3102
4424
  return { diagnostics, graph, written, stats };
3103
4425
  }
3104
4426
  async function checkProject(options) {
3105
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4427
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3106
4428
  const diagnostics = [
3107
4429
  ...graph.diagnostics ?? [],
3108
4430
  ...validateGraph(graph, {
@@ -3121,6 +4443,7 @@ async function checkProject(options) {
3121
4443
  diagnostic.severity = "error";
3122
4444
  }
3123
4445
  }
4446
+ const typeSafety = resolveTypeSafety(options);
3124
4447
  const rendered = renderApplication(graph, {
3125
4448
  rootDir: options.rootDir,
3126
4449
  outDir: options.outDir,
@@ -3128,6 +4451,22 @@ async function checkProject(options) {
3128
4451
  generatePermissions: options.generatePermissions,
3129
4452
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3130
4453
  });
4454
+ if (typeSafety.scanProductionSource) {
4455
+ diagnostics.push(...scanProductionSource({
4456
+ rootDir: options.rootDir,
4457
+ include: options.include,
4458
+ outDir: options.outDir,
4459
+ strict: options.strict,
4460
+ ...typeSafety
4461
+ }));
4462
+ }
4463
+ if (typeSafety.noAnyInGenerated) {
4464
+ diagnostics.push(...scanGeneratedArtifacts({
4465
+ "application.ts": rendered.applicationCode,
4466
+ "client.ts": rendered.clientCode,
4467
+ "permissions.ts": rendered.permissionsCode
4468
+ }, options.strict ?? false));
4469
+ }
3131
4470
  const expectedFiles = {
3132
4471
  "application.ts": rendered.applicationCode,
3133
4472
  "app.manifest.json": rendered.manifestJson
@@ -3140,12 +4479,12 @@ async function checkProject(options) {
3140
4479
  }
3141
4480
  const mismatches = [];
3142
4481
  for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
3143
- const diskPath = join3(options.outDir, filename);
3144
- if (!existsSync2(diskPath)) {
4482
+ const diskPath = join4(options.outDir, filename);
4483
+ if (!existsSync3(diskPath)) {
3145
4484
  mismatches.push(`${filename}: generated artifact is missing from disk`);
3146
4485
  continue;
3147
4486
  }
3148
- const diskContent = readFileSync(diskPath, "utf8");
4487
+ const diskContent = readFileSync3(diskPath, "utf8");
3149
4488
  if (diskContent !== expectedContent) {
3150
4489
  mismatches.push(`${filename}: disk artifact differs from current compiler output`);
3151
4490
  }
@@ -3157,29 +4496,40 @@ async function checkProject(options) {
3157
4496
  graph
3158
4497
  };
3159
4498
  }
4499
+ function resolveTypeSafety(options) {
4500
+ return {
4501
+ noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
4502
+ scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
4503
+ exclude: options.typeSafety?.exclude
4504
+ };
4505
+ }
3160
4506
  // src/watch.ts
3161
4507
  import { watch } from "node:fs";
3162
- import { relative as relative3, resolve as resolve2 } from "node:path";
4508
+ import { relative as relative4, resolve as resolve4 } from "node:path";
3163
4509
 
3164
4510
  // 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";
4511
+ import { createHash as createHash5 } from "node:crypto";
4512
+ import { access as access2, readdir, readFile } from "node:fs/promises";
4513
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
3168
4514
  function createDependencyGraphCache() {
3169
4515
  return {
3170
4516
  modules: new Map,
3171
- fileHashes: new Map
4517
+ fileHashes: new Map,
4518
+ generatedHashes: new Map
3172
4519
  };
3173
4520
  }
3174
4521
  function createIncrementalCompiler() {
3175
4522
  let previousSnapshot;
3176
4523
  let previousResult;
4524
+ let previousCache;
3177
4525
  const cache = createDependencyGraphCache();
3178
4526
  return {
3179
4527
  async compile(options, changedPaths) {
3180
- const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
4528
+ const optionsKey = optionsKeyOf(options);
4529
+ const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
3181
4530
  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);
4531
+ const activeCache = options.cache ?? cache;
4532
+ const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
3183
4533
  if (cacheHit && previousResult) {
3184
4534
  return {
3185
4535
  ...previousResult,
@@ -3192,22 +4542,14 @@ function createIncrementalCompiler() {
3192
4542
  }
3193
4543
  };
3194
4544
  }
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
4545
  if (!activeCache.dependencyGraph && previousResult) {
3208
4546
  activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
3209
4547
  }
3210
- const result = await compileProject({ ...options, cache: activeCache });
4548
+ const result = await compileProject({
4549
+ ...options,
4550
+ cache: activeCache,
4551
+ changedPaths: changedFiles
4552
+ });
3211
4553
  const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
3212
4554
  const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
3213
4555
  const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
@@ -3223,14 +4565,18 @@ function createIncrementalCompiler() {
3223
4565
  };
3224
4566
  previousSnapshot = snapshot;
3225
4567
  previousResult = result;
4568
+ previousCache = activeCache;
3226
4569
  return { ...result, stats };
3227
4570
  },
3228
4571
  reset() {
3229
4572
  previousSnapshot = undefined;
3230
4573
  previousResult = undefined;
4574
+ previousCache = undefined;
3231
4575
  cache.modules.clear();
3232
4576
  cache.fileHashes.clear();
4577
+ cache.generatedHashes?.clear();
3233
4578
  cache.dependencyGraph = undefined;
4579
+ cache.programSession?.reset();
3234
4580
  },
3235
4581
  getCache() {
3236
4582
  return cache;
@@ -3238,18 +4584,21 @@ function createIncrementalCompiler() {
3238
4584
  };
3239
4585
  }
3240
4586
  async function updateSnapshot(previous, options, changedPaths) {
3241
- const rootDir = resolve(options.rootDir);
3242
- const outDir = resolve(options.outDir);
4587
+ const rootDir = resolve3(options.rootDir);
4588
+ const outDir = resolve3(options.outDir);
3243
4589
  const files = { ...previous.files };
3244
4590
  for (const changedPath of changedPaths) {
3245
- const absolutePath = resolve(rootDir, changedPath);
4591
+ const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4592
+ const relativeChangedPath = relative3(rootDir, absolutePath);
4593
+ if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
4594
+ continue;
3246
4595
  if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
3247
4596
  continue;
3248
- const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
4597
+ const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
3249
4598
  try {
3250
- await access(absolutePath);
4599
+ await access2(absolutePath);
3251
4600
  const content = await readFile(absolutePath);
3252
- files[relativePath] = createHash2("sha256").update(content).digest("hex");
4601
+ files[relativePath] = createHash5("sha256").update(content).digest("hex");
3253
4602
  } catch {
3254
4603
  delete files[relativePath];
3255
4604
  }
@@ -3257,20 +4606,23 @@ async function updateSnapshot(previous, options, changedPaths) {
3257
4606
  return { files, optionsKey: optionsKeyOf(options) };
3258
4607
  }
3259
4608
  async function createSnapshot(options) {
3260
- const rootDir = resolve(options.rootDir);
3261
- const outDir = resolve(options.outDir);
4609
+ const rootDir = resolve3(options.rootDir);
4610
+ const outDir = resolve3(options.outDir);
3262
4611
  const paths = await listSourceFiles(rootDir, outDir);
3263
4612
  const files = {};
3264
4613
  for (const path of paths) {
3265
4614
  const content = await readFile(path);
3266
- files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
4615
+ files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
3267
4616
  }
3268
4617
  return { files, optionsKey: optionsKeyOf(options) };
3269
4618
  }
3270
4619
  function optionsKeyOf(options) {
3271
4620
  return JSON.stringify({
4621
+ rootDir: resolve3(options.rootDir),
4622
+ outDir: resolve3(options.outDir),
3272
4623
  include: options.include,
3273
4624
  strict: options.strict,
4625
+ writeOnError: options.writeOnError,
3274
4626
  moduleBoundaryPreset: options.moduleBoundaryPreset,
3275
4627
  moduleBoundaries: options.moduleBoundaries,
3276
4628
  allowRouteCommandBindings: options.allowRouteCommandBindings,
@@ -3278,27 +4630,16 @@ function optionsKeyOf(options) {
3278
4630
  disallowControllerDirectDb: options.disallowControllerDirectDb,
3279
4631
  detectOrphanModules: options.detectOrphanModules,
3280
4632
  generateClient: options.generateClient,
3281
- generatePermissions: options.generatePermissions
4633
+ generatePermissions: options.generatePermissions,
4634
+ typeSafety: options.typeSafety,
4635
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
3282
4636
  });
3283
4637
  }
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
4638
  async function listSourceFiles(rootDir, outDir) {
3298
4639
  const result = [];
3299
4640
  const visit = async (directory) => {
3300
4641
  for (const entry of await readdir(directory, { withFileTypes: true })) {
3301
- const path = resolve(directory, entry.name);
4642
+ const path = resolve3(directory, entry.name);
3302
4643
  if (entry.isDirectory()) {
3303
4644
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
3304
4645
  continue;
@@ -3356,7 +4697,9 @@ class ModuleDependencyGraph {
3356
4697
  if (!this.dependents.has(imp)) {
3357
4698
  this.dependents.set(imp, new Set);
3358
4699
  }
3359
- this.dependents.get(imp).add(modName);
4700
+ const dependents = this.dependents.get(imp);
4701
+ if (dependents)
4702
+ dependents.add(modName);
3360
4703
  }
3361
4704
  }
3362
4705
  }
@@ -3367,7 +4710,9 @@ class ModuleDependencyGraph {
3367
4710
  if (!this.fileOwners.has(normalized)) {
3368
4711
  this.fileOwners.set(normalized, new Set);
3369
4712
  }
3370
- this.fileOwners.get(normalized).add(moduleName);
4713
+ const owners = this.fileOwners.get(normalized);
4714
+ if (owners)
4715
+ owners.add(moduleName);
3371
4716
  }
3372
4717
  getModulesOwningFile(filePath) {
3373
4718
  const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
@@ -3383,12 +4728,14 @@ class ModuleDependencyGraph {
3383
4728
  }
3384
4729
  }
3385
4730
  if (directlyAffected.size === 0) {
3386
- return Array.from(this.moduleMap.keys());
4731
+ return [];
3387
4732
  }
3388
4733
  const affected = new Set(directlyAffected);
3389
4734
  const queue = Array.from(directlyAffected);
3390
4735
  while (queue.length > 0) {
3391
4736
  const current = queue.shift();
4737
+ if (!current)
4738
+ continue;
3392
4739
  const dependents = this.dependents.get(current);
3393
4740
  if (dependents) {
3394
4741
  for (const dep of dependents) {
@@ -3416,8 +4763,8 @@ function findAffectedModules(previous, current, changedFiles) {
3416
4763
  // src/watch.ts
3417
4764
  var DEFAULT_DEBOUNCE_MS = 100;
3418
4765
  function watchProject(options) {
3419
- const rootDir = resolve2(options.rootDir);
3420
- const outDir = resolve2(options.outDir);
4766
+ const rootDir = resolve4(options.rootDir);
4767
+ const outDir = resolve4(options.outDir);
3421
4768
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3422
4769
  let timer;
3423
4770
  let closed = false;
@@ -3427,8 +4774,12 @@ function watchProject(options) {
3427
4774
  let watcher;
3428
4775
  const incremental = createIncrementalCompiler();
3429
4776
  let initialEvent;
3430
- let resolveReady;
3431
- let rejectReady;
4777
+ let resolveReady = () => {
4778
+ return;
4779
+ };
4780
+ let rejectReady = () => {
4781
+ return;
4782
+ };
3432
4783
  const ready = new Promise((resolvePromise, rejectPromise) => {
3433
4784
  resolveReady = resolvePromise;
3434
4785
  rejectReady = rejectPromise;
@@ -3497,12 +4848,12 @@ function watchProject(options) {
3497
4848
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
3498
4849
  if (!filename)
3499
4850
  return schedule();
3500
- const changedPath = resolve2(rootDir, filename.toString());
3501
- const relativePath = relative3(outDir, changedPath);
4851
+ const changedPath = resolve4(rootDir, filename.toString());
4852
+ const relativePath = relative4(outDir, changedPath);
3502
4853
  if (!relativePath.startsWith("..") && relativePath !== "")
3503
4854
  return;
3504
4855
  if (/\.(tsx?|mts|cts)$/.test(changedPath))
3505
- schedule(relative3(rootDir, changedPath));
4856
+ schedule(relative4(rootDir, changedPath));
3506
4857
  });
3507
4858
  if (initialEvent)
3508
4859
  resolveReady(initialEvent);
@@ -3523,8 +4874,8 @@ function watchProject(options) {
3523
4874
  };
3524
4875
  }
3525
4876
  // src/inspect.ts
3526
- import { existsSync as existsSync3 } from "node:fs";
3527
- import { join as join4 } from "node:path";
4877
+ import { existsSync as existsSync4 } from "node:fs";
4878
+ import { join as join5 } from "node:path";
3528
4879
  function formatGraph(graph) {
3529
4880
  const lines = [];
3530
4881
  for (const module of graph.modules) {
@@ -3565,13 +4916,13 @@ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
3565
4916
  const checks = [
3566
4917
  {
3567
4918
  name: "project-root",
3568
- ok: existsSync3(rootDir),
3569
- detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
4919
+ ok: existsSync4(rootDir),
4920
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
3570
4921
  },
3571
4922
  {
3572
4923
  name: "tsconfig",
3573
- ok: existsSync3(join4(rootDir, "tsconfig.json")),
3574
- detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
4924
+ ok: existsSync4(join5(rootDir, "tsconfig.json")),
4925
+ detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
3575
4926
  },
3576
4927
  {
3577
4928
  name: "modules",
@@ -3665,12 +5016,15 @@ export {
3665
5016
  MODULAR_MONOLITH_RULES,
3666
5017
  MODULE_BOUNDARY_PROFILES,
3667
5018
  ModuleDependencyGraph,
5019
+ TraitCompiler,
3668
5020
  analyzeProject,
3669
5021
  camelName,
3670
5022
  checkProject,
3671
5023
  compileProject,
5024
+ compileTraits,
3672
5025
  createDependencyGraphCache,
3673
5026
  createIncrementalCompiler,
5027
+ createIncrementalProgramSession,
3674
5028
  doctorProject,
3675
5029
  explainGraph,
3676
5030
  exportGraphDot,
@@ -3681,6 +5035,8 @@ export {
3681
5035
  getModuleBoundaryProfile,
3682
5036
  renderApplication,
3683
5037
  resolveModuleBoundaries,
5038
+ scanGeneratedArtifacts,
5039
+ scanProductionSource,
3684
5040
  validateGraph,
3685
5041
  watchProject
3686
5042
  };