@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/cli.js CHANGED
@@ -1,17 +1,335 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve3 } from "node:path";
4
+ import { resolve as resolve5 } from "node:path";
5
5
 
6
6
  // src/analyze.ts
7
- import { existsSync } from "node:fs";
7
+ import { createHash as createHash3 } from "node:crypto";
8
+ import { relative, resolve as resolvePath, sep } from "node:path";
9
+ import * as ts3 from "@typescript/typescript6";
10
+
11
+ // src/program.ts
12
+ import { createHash as createHash2 } from "node:crypto";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import * as ts2 from "@typescript/typescript6";
16
+
17
+ // src/traits.ts
8
18
  import { createHash } from "node:crypto";
9
- import { join, relative, sep } from "node:path";
10
- import {
11
- Node,
12
- Project,
13
- SyntaxKind
14
- } from "ts-morph";
19
+ import * as ts from "@typescript/typescript6";
20
+ class TraitCompiler {
21
+ handlers;
22
+ constructor(handlers = createDefaultTraitHandlers()) {
23
+ this.handlers = handlers;
24
+ }
25
+ compile(program, previous, changedFiles) {
26
+ const byFile = new Map;
27
+ for (const sourceFile of program.getSourceFiles()) {
28
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("/node_modules/"))
29
+ continue;
30
+ const previousTraits = previous?.byFile.get(sourceFile.fileName);
31
+ if (previousTraits && !changedFiles.has(sourceFile.fileName)) {
32
+ byFile.set(sourceFile.fileName, previousTraits);
33
+ continue;
34
+ }
35
+ byFile.set(sourceFile.fileName, this.compileSourceFile(sourceFile));
36
+ }
37
+ const all = [...byFile.values()].flat().sort((a, b) => a.file.localeCompare(b.file) || a.start - b.start || a.kind.localeCompare(b.kind));
38
+ return { byFile, all };
39
+ }
40
+ compileSourceFile(sourceFile) {
41
+ const traits = [];
42
+ const visit = (node) => {
43
+ for (const handler of this.handlers) {
44
+ const name = handler.detect(node);
45
+ if (name)
46
+ traits.push(record(handler.kind, name, sourceFile, node));
47
+ }
48
+ ts.forEachChild(node, visit);
49
+ };
50
+ visit(sourceFile);
51
+ return traits;
52
+ }
53
+ }
54
+ function compileTraits(program, previous, changedFiles) {
55
+ return new TraitCompiler().compile(program, previous, changedFiles);
56
+ }
57
+ function record(kind, name, sourceFile, node) {
58
+ const text = node.getText(sourceFile);
59
+ return {
60
+ kind,
61
+ name,
62
+ file: sourceFile.fileName,
63
+ start: node.getStart(sourceFile),
64
+ end: node.end,
65
+ fingerprint: createHash("sha1").update(`${kind}:${text}`).digest("hex")
66
+ };
67
+ }
68
+ function decoratorName(decorator) {
69
+ return expressionName(ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression);
70
+ }
71
+ function expressionName(expression) {
72
+ if (ts.isIdentifier(expression))
73
+ return expression.text;
74
+ if (ts.isPropertyAccessExpression(expression))
75
+ return expression.name.text;
76
+ return "";
77
+ }
78
+ function createDefaultTraitHandlers() {
79
+ return [
80
+ {
81
+ kind: "module",
82
+ detect: decoratedDeclaration("Module")
83
+ },
84
+ {
85
+ kind: "injectable",
86
+ detect: decoratedDeclaration("Injectable")
87
+ },
88
+ {
89
+ kind: "controller",
90
+ detect: decoratedDeclaration("Controller")
91
+ },
92
+ {
93
+ kind: "command",
94
+ detect: decoratedDeclaration("Command")
95
+ },
96
+ {
97
+ kind: "query",
98
+ detect: decoratedDeclaration("Query")
99
+ },
100
+ {
101
+ kind: "defineModule",
102
+ detect: (node) => {
103
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
104
+ return;
105
+ const initializer = node.initializer;
106
+ return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineModule" ? node.name.text : undefined;
107
+ }
108
+ },
109
+ {
110
+ kind: "injectionToken",
111
+ detect: (node) => {
112
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
113
+ return;
114
+ const initializer = node.initializer;
115
+ return initializer && ts.isNewExpression(initializer) && expressionName(initializer.expression) === "InjectionToken" ? node.name.text : undefined;
116
+ }
117
+ }
118
+ ];
119
+ }
120
+ function decoratedDeclaration(decorator) {
121
+ return (node) => {
122
+ if (!ts.isClassDeclaration(node) || !node.name)
123
+ return;
124
+ return (ts.getDecorators(node) ?? []).some((item) => decoratorName(item) === decorator) ? node.name.text : undefined;
125
+ };
126
+ }
127
+
128
+ // src/program.ts
129
+ function createIncrementalProgramSession(projectRoot) {
130
+ const rootDir = resolve(projectRoot);
131
+ let projectConfig = readProjectConfig(rootDir);
132
+ let projectConfigKey = configKey(projectConfig);
133
+ let builder;
134
+ let traits;
135
+ const sourceFileCache = new Map;
136
+ return {
137
+ getProgram() {
138
+ if (!builder) {
139
+ throw new Error("incremental TypeScript program has not been initialized");
140
+ }
141
+ return builder.getProgram();
142
+ },
143
+ getTypeChecker() {
144
+ return this.getProgram().getTypeChecker();
145
+ },
146
+ update(rootNames, changedPaths = rootNames) {
147
+ const oldProgram = builder?.getProgram();
148
+ const oldSourceFiles = new Map(oldProgram?.getSourceFiles().map((sourceFile) => [canonical(sourceFile.fileName), sourceFile]) ?? []);
149
+ const nextProjectConfig = readProjectConfig(rootDir);
150
+ const nextProjectConfigKey = configKey(nextProjectConfig);
151
+ const configChanged = nextProjectConfigKey !== projectConfigKey;
152
+ const previousBuilder = configChanged ? undefined : builder;
153
+ if (configChanged) {
154
+ sourceFileCache.clear();
155
+ traits = undefined;
156
+ }
157
+ projectConfig = nextProjectConfig;
158
+ projectConfigKey = nextProjectConfigKey;
159
+ const normalizedRoots = [...new Set(rootNames.map((file) => resolve(rootDir, file)))].sort();
160
+ const normalizedChanged = [...new Set(changedPaths.map((file) => resolve(rootDir, file)))];
161
+ const invalidatedPaths = new Set(normalizedChanged.map(canonical));
162
+ for (const sourceFile of oldSourceFiles.values()) {
163
+ if (sourceVersion(sourceFile.fileName) !== sourceFileVersion(sourceFile)) {
164
+ invalidatedPaths.add(canonical(sourceFile.fileName));
165
+ }
166
+ }
167
+ const invalidateAllResolutions = configChanged || [...invalidatedPaths].some((fileName) => {
168
+ const wasInProgram = oldSourceFiles.has(fileName);
169
+ return wasInProgram !== existsSync(fileName);
170
+ });
171
+ for (const fileName of normalizedChanged) {
172
+ if (!existsSync(fileName))
173
+ sourceFileCache.delete(canonical(fileName));
174
+ }
175
+ const host = createHost(projectConfig.options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions);
176
+ builder = ts2.createEmitAndSemanticDiagnosticsBuilderProgram(normalizedRoots, projectConfig.options, host, previousBuilder, projectConfig.errors, projectConfig.projectReferences);
177
+ const program = builder.getProgram();
178
+ const changedFiles = [];
179
+ const reusedFiles = [];
180
+ const currentPaths = new Set(program.getSourceFiles().map((file) => canonical(file.fileName)));
181
+ for (const sourceFile of program.getSourceFiles()) {
182
+ if (sourceFile.isDeclarationFile)
183
+ continue;
184
+ const previous = oldSourceFiles.get(canonical(sourceFile.fileName));
185
+ if (previous && previous === sourceFile) {
186
+ reusedFiles.push(sourceFile.fileName);
187
+ } else {
188
+ changedFiles.push(sourceFile.fileName);
189
+ }
190
+ }
191
+ for (const [path, sourceFile] of oldSourceFiles) {
192
+ if (!sourceFile.isDeclarationFile && !currentPaths.has(path)) {
193
+ changedFiles.push(sourceFile.fileName);
194
+ }
195
+ }
196
+ for (const path of sourceFileCache.keys()) {
197
+ if (!currentPaths.has(path))
198
+ sourceFileCache.delete(path);
199
+ }
200
+ traits = compileTraits(program, traits, new Set(changedFiles));
201
+ return { changedFiles, reusedFiles, program };
202
+ },
203
+ getTraits() {
204
+ return traits?.all ?? [];
205
+ },
206
+ getDiagnostics() {
207
+ if (!builder)
208
+ return projectConfig.errors;
209
+ const program = builder.getProgram();
210
+ return [
211
+ ...projectConfig.errors,
212
+ ...program.getSyntacticDiagnostics()
213
+ ];
214
+ },
215
+ emit() {
216
+ if (!builder) {
217
+ throw new Error("incremental TypeScript program has not been initialized");
218
+ }
219
+ return builder.emit();
220
+ },
221
+ reset() {
222
+ builder = undefined;
223
+ projectConfig = readProjectConfig(rootDir);
224
+ projectConfigKey = configKey(projectConfig);
225
+ traits = undefined;
226
+ sourceFileCache.clear();
227
+ }
228
+ };
229
+ }
230
+ function createHost(options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions) {
231
+ const host = ts2.createIncrementalCompilerHost(options, {
232
+ ...ts2.sys,
233
+ getCurrentDirectory: () => rootDir
234
+ });
235
+ host.hasInvalidatedResolutions = (filePath) => invalidateAllResolutions || invalidatedPaths.has(canonical(filePath));
236
+ const originalGetSourceFile = host.getSourceFile.bind(host);
237
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
238
+ const key = canonical(fileName);
239
+ const text = host.readFile(fileName);
240
+ if (text === undefined) {
241
+ sourceFileCache.delete(key);
242
+ return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
243
+ }
244
+ const version = hashText(text);
245
+ const parseKey = sourceFileParseKey(languageVersion);
246
+ const cached = sourceFileCache.get(key);
247
+ if (!shouldCreateNewSourceFile && cached?.version === version && cached.parseKey === parseKey) {
248
+ return cached.sourceFile;
249
+ }
250
+ const sourceFile = originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
251
+ if (sourceFile) {
252
+ sourceFileCache.set(key, { sourceFile, version: hashText(sourceFile.text), parseKey });
253
+ } else {
254
+ sourceFileCache.delete(key);
255
+ }
256
+ return sourceFile;
257
+ };
258
+ return host;
259
+ }
260
+ function canonical(fileName) {
261
+ const normalized = resolve(fileName);
262
+ return ts2.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();
263
+ }
264
+ function sourceFileParseKey(languageVersion) {
265
+ return typeof languageVersion === "number" ? `target:${languageVersion}` : JSON.stringify({
266
+ languageVersion: languageVersion.languageVersion,
267
+ impliedNodeFormat: languageVersion.impliedNodeFormat,
268
+ jsDocParsingMode: languageVersion.jsDocParsingMode
269
+ });
270
+ }
271
+ function configKey(config) {
272
+ return JSON.stringify({
273
+ options: config.options,
274
+ projectReferences: config.projectReferences,
275
+ configFingerprint: config.configFingerprint
276
+ });
277
+ }
278
+ function hashText(text) {
279
+ return createHash2("sha1").update(text).digest("hex");
280
+ }
281
+ function sourceVersion(fileName) {
282
+ try {
283
+ return hashText(readFileSync(fileName, "utf8"));
284
+ } catch {
285
+ return "missing";
286
+ }
287
+ }
288
+ function sourceFileVersion(sourceFile) {
289
+ const descriptor = Object.getOwnPropertyDescriptor(sourceFile, "version");
290
+ return typeof descriptor?.value === "string" ? descriptor.value : undefined;
291
+ }
292
+ function readProjectConfig(rootDir) {
293
+ const configPath = join(rootDir, "tsconfig.json");
294
+ if (!existsSync(configPath)) {
295
+ return {
296
+ options: {
297
+ target: ts2.ScriptTarget.ES2022,
298
+ module: ts2.ModuleKind.ESNext,
299
+ moduleResolution: ts2.ModuleResolutionKind.Bundler,
300
+ experimentalDecorators: true,
301
+ allowJs: false,
302
+ skipLibCheck: true
303
+ },
304
+ errors: [],
305
+ projectReferences: undefined,
306
+ configFingerprint: "defaults"
307
+ };
308
+ }
309
+ const configReads = new Map;
310
+ const readConfig = (fileName) => {
311
+ const text = ts2.sys.readFile(fileName);
312
+ configReads.set(canonical(fileName), text === undefined ? "missing" : hashText(text));
313
+ return text;
314
+ };
315
+ const config = ts2.readConfigFile(configPath, readConfig);
316
+ if (config.error) {
317
+ return {
318
+ options: {},
319
+ errors: [config.error],
320
+ configFingerprint: JSON.stringify([...configReads])
321
+ };
322
+ }
323
+ const parsed = ts2.parseJsonConfigFileContent(config.config, { ...ts2.sys, readFile: readConfig }, dirname(configPath));
324
+ return {
325
+ options: parsed.options,
326
+ errors: parsed.errors,
327
+ projectReferences: parsed.projectReferences,
328
+ configFingerprint: JSON.stringify([...configReads])
329
+ };
330
+ }
331
+
332
+ // src/analyze.ts
15
333
  var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
16
334
  var ROUTE_DECORATORS = {
17
335
  Get: "GET",
@@ -23,63 +341,118 @@ var ROUTE_DECORATORS = {
23
341
  Options: "OPTIONS"
24
342
  };
25
343
  var SCOPES = ["application", "request", "job"];
26
- async function analyzeProject(rootDir, include, cache) {
27
- let project;
28
- if (cache?.project) {
29
- project = cache.project;
30
- for (const sf of project.getSourceFiles()) {
31
- sf.refreshFromFileSystemSync();
32
- }
33
- } else {
34
- project = createProject(rootDir);
35
- const patterns = (include ?? DEFAULT_INCLUDE).map((glob) => join(rootDir, glob));
36
- project.addSourceFilesAtPaths(patterns);
37
- if (cache)
38
- cache.project = project;
39
- }
40
- const sourceFiles = project.getSourceFiles().filter((sf) => !sf.getFilePath().includes("node_modules") && !sf.isDeclarationFile()).sort((a, b) => a.getFilePath().localeCompare(b.getFilePath()));
344
+ function isScope(value) {
345
+ return SCOPES.some((scope) => scope === value);
346
+ }
347
+ function nodeText(node) {
348
+ return node.getText(node.getSourceFile());
349
+ }
350
+ function lineOf(node) {
351
+ const sourceFile = node.getSourceFile();
352
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
353
+ }
354
+ function variableName(decl) {
355
+ return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
356
+ }
357
+ function propertyName(name) {
358
+ if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
359
+ return name.text;
360
+ if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
361
+ return name.text;
362
+ return nodeText(name);
363
+ }
364
+ function parameterName(param) {
365
+ return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
366
+ }
367
+ function decoratorsOf(node) {
368
+ return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
369
+ }
370
+ function decoratorArguments(dec) {
371
+ return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
372
+ }
373
+ function hasMethod(cls, name) {
374
+ return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
375
+ }
376
+ function hasDestroyHook(cls) {
377
+ return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
378
+ }
379
+ function descendantsOfKind(root, predicate) {
380
+ const result = [];
381
+ const visit = (node) => {
382
+ if (predicate(node))
383
+ result.push(node);
384
+ ts3.forEachChild(node, visit);
385
+ };
386
+ visit(root);
387
+ return result;
388
+ }
389
+ async function analyzeProject(rootDir, include, cache, changedPaths) {
390
+ const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
391
+ if (cache)
392
+ cache.programSession = session;
393
+ const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
394
+ const update = session.update(rootNames, changedPaths);
395
+ const program = update.program;
396
+ const checker = program.getTypeChecker();
397
+ 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));
41
398
  const ctx = {
42
399
  rootDir,
400
+ program,
401
+ checker,
43
402
  tokensByName: new Map,
44
403
  classesByName: new Map,
45
404
  diagnostics: []
46
405
  };
406
+ const nativeTraitFiles = new Map;
407
+ for (const diagnostic of session.getDiagnostics()) {
408
+ ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
409
+ }
410
+ for (const trait of session.getTraits()) {
411
+ const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
412
+ kinds.add(trait.kind);
413
+ nativeTraitFiles.set(trait.file, kinds);
414
+ }
47
415
  for (const sf of sourceFiles) {
48
416
  indexFile(sf, ctx);
49
417
  }
50
418
  const candidates = [];
51
419
  for (const sf of sourceFiles) {
52
- for (const cls of sf.getClasses()) {
53
- const moduleDec = findDecorator(cls, "Module");
54
- if (!moduleDec)
55
- continue;
56
- const options = decoratorObjectArg(moduleDec);
57
- if (!options)
58
- continue;
59
- candidates.push({
60
- node: cls,
61
- options,
62
- className: cls.getName() ?? "<anonymous>",
63
- file: sf.getFilePath(),
64
- line: cls.getStartLineNumber()
65
- });
420
+ const traits = nativeTraitFiles.get(sf.fileName);
421
+ if (!cache || traits?.has("module")) {
422
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
423
+ const moduleDec = findDecorator(cls, "Module");
424
+ if (!moduleDec)
425
+ continue;
426
+ const options = decoratorObjectArg(moduleDec);
427
+ if (!options)
428
+ continue;
429
+ candidates.push({
430
+ node: cls,
431
+ options,
432
+ className: cls.name?.text ?? "<anonymous>",
433
+ file: sf.fileName,
434
+ line: lineOf(cls)
435
+ });
436
+ }
66
437
  }
67
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
68
- if (call.getExpression().getText() !== "defineModule")
69
- continue;
70
- const parent = call.getParent();
71
- if (!parent || !Node.isVariableDeclaration(parent))
72
- continue;
73
- const arg = call.getArguments()[0];
74
- if (!arg || !Node.isObjectLiteralExpression(arg))
75
- continue;
76
- candidates.push({
77
- node: parent,
78
- options: arg,
79
- className: parent.getName(),
80
- file: sf.getFilePath(),
81
- line: parent.getStartLineNumber()
82
- });
438
+ if (!cache || traits?.has("defineModule")) {
439
+ for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
440
+ if (nodeText(call.expression) !== "defineModule")
441
+ continue;
442
+ const parent = call.parent;
443
+ if (!parent || !ts3.isVariableDeclaration(parent))
444
+ continue;
445
+ const arg = call.arguments[0];
446
+ if (!arg || !ts3.isObjectLiteralExpression(arg))
447
+ continue;
448
+ candidates.push({
449
+ node: parent,
450
+ options: arg,
451
+ className: variableName(parent),
452
+ file: sf.fileName,
453
+ line: lineOf(parent)
454
+ });
455
+ }
83
456
  }
84
457
  }
85
458
  const nameByNode = new Map;
@@ -92,8 +465,8 @@ async function analyzeProject(rootDir, include, cache) {
92
465
  if (cache) {
93
466
  const currentFileHashes = new Map;
94
467
  for (const sf of sourceFiles) {
95
- const rel = sourcePath(rootDir, sf.getFilePath());
96
- const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
468
+ const rel = sourcePath(rootDir, sf.fileName);
469
+ const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
97
470
  currentFileHashes.set(rel, hash);
98
471
  }
99
472
  const changedFiles = new Set;
@@ -109,7 +482,7 @@ async function analyzeProject(rootDir, include, cache) {
109
482
  }
110
483
  const modulesToKeep = new Map;
111
484
  const finalModules = [];
112
- const finalDiagnostics = [];
485
+ const finalDiagnostics = [...ctx.diagnostics];
113
486
  const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
114
487
  for (const [modName, entry] of cache.modules.entries()) {
115
488
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
@@ -131,14 +504,7 @@ async function analyzeProject(rootDir, include, cache) {
131
504
  const diagBefore = ctx.diagnostics.length;
132
505
  const parsed = parseModule(c, nameByNode, ctx);
133
506
  const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
134
- const ownedFiles = new Set;
135
- ownedFiles.add(parsed.file);
136
- for (const p of parsed.providers)
137
- if (p.file)
138
- ownedFiles.add(p.file);
139
- for (const ctrl of parsed.controllers)
140
- if (ctrl.file)
141
- ownedFiles.add(ctrl.file);
507
+ const ownedFiles = collectModuleSourceClosure(parsed, ctx);
142
508
  const fileHashes = {};
143
509
  for (const f of ownedFiles) {
144
510
  fileHashes[f] = currentFileHashes.get(f) ?? "";
@@ -165,6 +531,7 @@ async function analyzeProject(rootDir, include, cache) {
165
531
  } else {
166
532
  modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
167
533
  }
534
+ modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
168
535
  const allRegisteredClasses = new Set;
169
536
  const allRegisteredControllers = new Set;
170
537
  const allRegisteredCommands = new Set;
@@ -189,9 +556,9 @@ async function analyzeProject(rootDir, include, cache) {
189
556
  if (!allRegisteredClasses.has(name)) {
190
557
  const injectable = parseInjectableOptions(classInfo.decl, ctx);
191
558
  if (injectable?.providedIn === "root") {
192
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
559
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
193
560
  const file = sourcePath(ctx.rootDir, classInfo.file);
194
- const line = classInfo.decl.getStartLineNumber();
561
+ const line = lineOf(classInfo.decl);
195
562
  if (missing) {
196
563
  warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
197
564
  }
@@ -206,8 +573,9 @@ async function analyzeProject(rootDir, include, cache) {
206
573
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
207
574
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
208
575
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
576
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
209
577
  providedIn: "root",
210
- hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
578
+ hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
211
579
  exported: true,
212
580
  file,
213
581
  line,
@@ -218,8 +586,8 @@ async function analyzeProject(rootDir, include, cache) {
218
586
  if (!allRegisteredControllers.has(name)) {
219
587
  const controllerDec = findDecorator(classInfo.decl, "Controller");
220
588
  if (controllerDec) {
221
- const arg = controllerDec.getArguments()[0];
222
- const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
589
+ const arg = decoratorArguments(controllerDec)[0];
590
+ const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
223
591
  if (isStandalone) {
224
592
  const ctrl = parseController(classInfo.decl, ctx);
225
593
  if (ctrl)
@@ -233,13 +601,14 @@ async function analyzeProject(rootDir, include, cache) {
233
601
  const meta = decoratorObjectArg(commandDec);
234
602
  if (meta && booleanProp(meta, "standalone")) {
235
603
  standaloneCommands.push({
236
- className: classInfo.decl.getName() ?? name,
237
- name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
604
+ className: classInfo.decl.name?.text ?? name,
605
+ name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
238
606
  permission: stringLiteralProp(meta, "permission"),
239
607
  transaction: commandModeProp(meta, "transaction") ?? "none",
240
608
  audit: stringLiteralProp(meta, "audit"),
241
609
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
242
- standalone: true
610
+ standalone: true,
611
+ aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
243
612
  });
244
613
  }
245
614
  }
@@ -264,13 +633,13 @@ async function analyzeProject(rootDir, include, cache) {
264
633
  if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
265
634
  const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
266
635
  if (existingRoot) {
267
- existingRoot.providers.push(...rootProviders);
268
- existingRoot.controllers.push(...standaloneControllers);
269
- existingRoot.commands.push(...standaloneCommands);
270
- for (const p of rootProviders) {
271
- if (!existingRoot.exports.includes(p.token))
272
- existingRoot.exports.push(p.token);
273
- }
636
+ modules = modules.map((module) => module === existingRoot ? {
637
+ ...module,
638
+ providers: [...module.providers, ...rootProviders],
639
+ controllers: [...module.controllers, ...standaloneControllers],
640
+ commands: [...module.commands, ...standaloneCommands],
641
+ exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
642
+ } : module);
274
643
  } else {
275
644
  const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
276
645
  modules.unshift({
@@ -309,25 +678,72 @@ async function analyzeProject(rootDir, include, cache) {
309
678
  cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
310
679
  };
311
680
  }
312
- function createProject(rootDir) {
313
- const tsConfigFilePath = join(rootDir, "tsconfig.json");
314
- if (existsSync(tsConfigFilePath)) {
315
- return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
681
+ function collectModuleSourceClosure(module, ctx) {
682
+ const seeds = new Set;
683
+ const addRelativeModule = (path) => {
684
+ if (!path)
685
+ return;
686
+ const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
687
+ seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
688
+ };
689
+ addRelativeModule(module.file);
690
+ for (const provider of module.providers)
691
+ addRelativeModule(provider.importPath);
692
+ for (const controller of module.controllers)
693
+ addRelativeModule(controller.importPath);
694
+ const ownedFiles = new Set;
695
+ const queue = [...seeds];
696
+ while (queue.length > 0) {
697
+ const fileName = queue.shift();
698
+ if (!fileName)
699
+ continue;
700
+ const sourceFile = ctx.program.getSourceFile(fileName);
701
+ if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
702
+ continue;
703
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
704
+ if (ownedFiles.has(relativeFile))
705
+ continue;
706
+ ownedFiles.add(relativeFile);
707
+ for (const statement of sourceFile.statements) {
708
+ let moduleName;
709
+ if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
710
+ moduleName = statement.moduleSpecifier.text;
711
+ } else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
712
+ moduleName = statement.moduleSpecifier.text;
713
+ } else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
714
+ moduleName = statement.moduleReference.expression.text;
715
+ }
716
+ if (!moduleName || moduleName.startsWith("node:"))
717
+ continue;
718
+ const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
719
+ if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
720
+ queue.push(resolved);
721
+ }
316
722
  }
317
- return new Project({
318
- compilerOptions: { experimentalDecorators: true, allowJs: false }
319
- });
723
+ return ownedFiles;
724
+ }
725
+ function resolveSourcePath(rootDir, file) {
726
+ const normalized = file.replace(/\\/g, "/");
727
+ return resolvePath(rootDir, normalized);
728
+ }
729
+ function isProjectSourcePath(fileName, rootDir) {
730
+ const normalized = fileName.replace(/\\/g, "/");
731
+ const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
732
+ return normalized === root || normalized.startsWith(`${root}/`);
733
+ }
734
+ function isProjectSourceFile(sourceFile, rootDir) {
735
+ return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
320
736
  }
321
737
  function indexFile(sf, ctx) {
322
- for (const cls of sf.getClasses()) {
323
- const name = cls.getName();
738
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
739
+ const name = cls.name?.text;
324
740
  if (name && !ctx.classesByName.has(name)) {
325
- ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
741
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
326
742
  }
327
743
  }
328
- for (const statement of sf.getVariableStatements()) {
329
- for (const decl of statement.getDeclarations()) {
330
- const info = parseTokenVariable(decl, sf.getFilePath());
744
+ for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
745
+ for (const decl of statement.declarationList.declarations) {
746
+ const info = parseTokenVariable(decl, sf.fileName);
331
747
  if (info && !ctx.tokensByName.has(info.name)) {
332
748
  ctx.tokensByName.set(info.name, info);
333
749
  }
@@ -335,19 +751,19 @@ function indexFile(sf, ctx) {
335
751
  }
336
752
  }
337
753
  function parseTokenVariable(decl, file) {
338
- const init = decl.getInitializer();
339
- if (!init || !Node.isNewExpression(init))
754
+ const init = decl.initializer;
755
+ if (!init || !ts3.isNewExpression(init))
340
756
  return;
341
- if (init.getExpression().getText() !== "InjectionToken")
757
+ if (nodeText(init.expression) !== "InjectionToken")
342
758
  return;
343
- const [nameArg, optionsArg] = init.getArguments();
344
- const info = { name: decl.getName(), file, line: decl.getStartLineNumber() };
345
- if (nameArg && Node.isStringLiteral(nameArg)) {
346
- info.stringName = nameArg.getLiteralText();
759
+ const [nameArg, optionsArg] = init.arguments ?? [];
760
+ const info = { name: variableName(decl), file, line: lineOf(decl) };
761
+ if (nameArg && ts3.isStringLiteral(nameArg)) {
762
+ info.stringName = nameArg.text;
347
763
  }
348
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
764
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
349
765
  const scope = stringLiteralProp(optionsArg, "scope");
350
- if (scope && SCOPES.includes(scope)) {
766
+ if (scope && isScope(scope)) {
351
767
  info.scope = scope;
352
768
  }
353
769
  const providedIn = stringLiteralProp(optionsArg, "providedIn");
@@ -364,33 +780,76 @@ function parseTokenVariable(decl, file) {
364
780
  function parseModule(candidate, nameByNode, ctx) {
365
781
  const { options, className, file, line } = candidate;
366
782
  const name = nameByNode.get(candidate.node) ?? className;
367
- const tags = arrayProp(options, "tags").map((el) => Node.isStringLiteral(el) ? el.getLiteralText() : el.getText().replace(/['"]/g, "")).filter(Boolean);
783
+ const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
784
+ const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
368
785
  const imports = arrayProp(options, "imports").map((el) => {
369
786
  const unwrapped = unwrapForwardRef(el);
370
- const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
787
+ const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
371
788
  if (decl) {
372
789
  const known = nameByNode.get(decl);
373
790
  if (known)
374
791
  return known;
375
- if (Node.isClassDeclaration(decl)) {
792
+ if (ts3.isClassDeclaration(decl)) {
376
793
  const dec = findDecorator(decl, "Module");
377
794
  const decOptions = dec && decoratorObjectArg(dec);
378
795
  const decName = decOptions && stringLiteralProp(decOptions, "name");
379
- return decName ?? decl.getName() ?? el.getText();
796
+ return decName ?? decl.name?.text ?? nodeText(el);
380
797
  }
381
- if (Node.isVariableDeclaration(decl))
382
- return decl.getName();
798
+ if (ts3.isVariableDeclaration(decl))
799
+ return variableName(decl);
383
800
  }
384
- return el.getText();
801
+ return nodeText(el);
385
802
  }).filter((v, i, arr) => arr.indexOf(v) === i);
386
803
  const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
387
804
  const exportsSet = new Set(exports);
388
805
  const providers = [];
389
- for (const el of arrayProp(options, "providers")) {
806
+ for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
807
+ const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
808
+ if (parsedProviders) {
809
+ providers.push(...parsedProviders);
810
+ continue;
811
+ }
812
+ if (ts3.isCallExpression(el)) {
813
+ const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
814
+ warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
815
+ continue;
816
+ }
390
817
  const provider = parseProvider(el, exportsSet, ctx);
391
818
  if (provider)
392
819
  providers.push(provider);
393
820
  }
821
+ for (const el of arrayProp(options, "jobs")) {
822
+ if (!ts3.isIdentifier(el))
823
+ continue;
824
+ const decl = resolveDeclaration(el, ctx)[0];
825
+ if (!decl || !ts3.isClassDeclaration(decl))
826
+ continue;
827
+ const className2 = decl.name?.text ?? el.text;
828
+ const registeredProvider = providers.find((provider) => provider.token === className2);
829
+ if (registeredProvider)
830
+ continue;
831
+ const deps = classDeps(decl, ctx);
832
+ const injectable = parseInjectableOptions(decl, ctx);
833
+ const scope = injectable?.scope === "application" ? "application" : "job";
834
+ providers.push({
835
+ token: className2,
836
+ tokenKind: "class",
837
+ kind: "class",
838
+ useClass: className2,
839
+ scope,
840
+ deps: deps.deps,
841
+ optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
842
+ selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
843
+ skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
844
+ hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
845
+ functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
846
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
847
+ exported: exportsSet.has(className2),
848
+ file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
849
+ line: lineOf(decl),
850
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
851
+ });
852
+ }
394
853
  const controllers = [];
395
854
  for (const el of arrayProp(options, "controllers")) {
396
855
  const controller = parseController(el, ctx);
@@ -400,40 +859,59 @@ function parseModule(candidate, nameByNode, ctx) {
400
859
  const handlerClasses = [];
401
860
  const seenHandlers = new Set;
402
861
  const collectHandler = (expr) => {
403
- if (!Node.isIdentifier(expr))
862
+ if (!ts3.isIdentifier(expr))
404
863
  return;
405
- const decl = resolveDeclaration(expr)[0];
406
- if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
407
- seenHandlers.add(decl.getName() ?? "");
864
+ const decl = resolveDeclaration(expr, ctx)[0];
865
+ if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
866
+ seenHandlers.add(decl.name?.text ?? "");
408
867
  handlerClasses.push(decl);
409
868
  }
410
869
  };
411
870
  for (const el of arrayProp(options, "providers")) {
412
- if (Node.isIdentifier(el))
871
+ if (ts3.isIdentifier(el))
413
872
  collectHandler(el);
414
- if (Node.isObjectLiteralExpression(el)) {
873
+ if (ts3.isObjectLiteralExpression(el)) {
415
874
  const useClass = getProp(el, "useClass");
416
875
  if (useClass)
417
876
  collectHandler(useClass);
418
877
  }
419
878
  }
420
879
  arrayProp(options, "commands").forEach(collectHandler);
880
+ arrayProp(options, "jobs").forEach(collectHandler);
421
881
  arrayProp(options, "queries").forEach(collectHandler);
422
882
  const commands = [];
883
+ const jobs = [];
423
884
  const queries = [];
424
885
  for (const cls of handlerClasses) {
425
886
  const commandDec = findDecorator(cls, "Command");
426
887
  if (commandDec) {
427
888
  const meta = decoratorObjectArg(commandDec);
428
889
  if (meta) {
890
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
429
891
  commands.push({
430
- className: cls.getName() ?? "<anonymous>",
431
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
892
+ className: cls.name?.text ?? "<anonymous>",
893
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
432
894
  permission: stringLiteralProp(meta, "permission"),
433
895
  transaction: commandModeProp(meta, "transaction") ?? "none",
434
896
  audit: stringLiteralProp(meta, "audit"),
435
897
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
436
- standalone: booleanProp(meta, "standalone") || undefined
898
+ ...booleanProp(meta, "standalone") ? { standalone: true } : {},
899
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
900
+ });
901
+ }
902
+ }
903
+ const jobDec = findDecorator(cls, "Job");
904
+ if (jobDec) {
905
+ const meta = decoratorObjectArg(jobDec);
906
+ if (meta) {
907
+ const injectable = parseInjectableOptions(cls, ctx);
908
+ const provider = providers.find((candidate2) => candidate2.token === (cls.name?.text ?? "<anonymous>"));
909
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${cls.name?.text ?? "<anonymous>"}`);
910
+ jobs.push({
911
+ className: cls.name?.text ?? "<anonymous>",
912
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
913
+ scope: provider?.scope ?? (injectable?.scope === "application" ? "application" : "job"),
914
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
437
915
  });
438
916
  }
439
917
  }
@@ -442,8 +920,8 @@ function parseModule(candidate, nameByNode, ctx) {
442
920
  const meta = decoratorObjectArg(queryDec);
443
921
  if (meta) {
444
922
  queries.push({
445
- className: cls.getName() ?? "<anonymous>",
446
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
923
+ className: cls.name?.text ?? "<anonymous>",
924
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
447
925
  });
448
926
  }
449
927
  }
@@ -458,7 +936,9 @@ function parseModule(candidate, nameByNode, ctx) {
458
936
  providers,
459
937
  controllers,
460
938
  commands,
939
+ jobs,
461
940
  queries,
941
+ ...aspects.length > 0 ? { aspects } : {},
462
942
  exports
463
943
  };
464
944
  }
@@ -467,14 +947,14 @@ function commandModeProp(object, name) {
467
947
  return value === "required" || value === "none" ? value : undefined;
468
948
  }
469
949
  function parseProvider(el, exportsSet, ctx) {
470
- const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
471
- const line = el.getStartLineNumber();
950
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
951
+ const line = lineOf(el);
472
952
  const unwrappedEl = unwrapForwardRef(el);
473
- if (Node.isIdentifier(unwrappedEl)) {
474
- const decl = resolveDeclaration(unwrappedEl)[0];
475
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
476
- const className = cls?.getName() ?? unwrappedEl.getText();
477
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], missing: false };
953
+ if (ts3.isIdentifier(unwrappedEl)) {
954
+ const decl = resolveDeclaration(unwrappedEl, ctx)[0];
955
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
956
+ const className = cls?.name?.text ?? unwrappedEl.text;
957
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
478
958
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
479
959
  if (missing) {
480
960
  warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
@@ -490,15 +970,16 @@ function parseProvider(el, exportsSet, ctx) {
490
970
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
491
971
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
492
972
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
973
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
493
974
  providedIn: injectable?.providedIn,
494
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
975
+ hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
495
976
  exported: exportsSet.has(className),
496
977
  file,
497
978
  line,
498
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
979
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
499
980
  };
500
981
  }
501
- if (!Node.isObjectLiteralExpression(el))
982
+ if (!ts3.isObjectLiteralExpression(el))
502
983
  return;
503
984
  const provideExpr = getProp(el, "provide");
504
985
  if (!provideExpr)
@@ -513,26 +994,36 @@ function parseProvider(el, exportsSet, ctx) {
513
994
  const useExistingExpr = getProp(el, "useExisting");
514
995
  if (useClassExpr) {
515
996
  const unwrappedClass = unwrapForwardRef(useClassExpr);
516
- const decl = Node.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass)[0] : undefined;
517
- const cls = decl && Node.isClassDeclaration(decl) ? decl : undefined;
518
- const useClass = cls?.getName() ?? unwrappedClass.getText();
997
+ const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
998
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
999
+ const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
519
1000
  let deps = explicitDeps;
520
1001
  let optionalDeps = [];
521
1002
  let selfDeps = [];
522
1003
  let skipSelfDeps = [];
523
1004
  let hostDeps = [];
524
- if (deps.length === 0 && cls) {
1005
+ let functionalInjects = [];
1006
+ if (cls) {
525
1007
  const result = classDeps(cls, ctx);
526
- deps = result.deps;
527
- optionalDeps = result.optionalDeps;
528
- selfDeps = result.selfDeps;
529
- skipSelfDeps = result.skipSelfDeps;
530
- hostDeps = result.hostDeps;
1008
+ if (deps.length === 0) {
1009
+ deps = result.deps;
1010
+ optionalDeps = result.optionalDeps;
1011
+ selfDeps = result.selfDeps;
1012
+ skipSelfDeps = result.skipSelfDeps;
1013
+ hostDeps = result.hostDeps;
1014
+ } else {
1015
+ optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
1016
+ selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
1017
+ skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
1018
+ hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
1019
+ }
1020
+ functionalInjects = result.functionalInjects;
531
1021
  if (result.missing) {
532
1022
  warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
533
1023
  }
534
1024
  }
535
1025
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
1026
+ validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
536
1027
  return {
537
1028
  token,
538
1029
  tokenKind,
@@ -544,35 +1035,38 @@ function parseProvider(el, exportsSet, ctx) {
544
1035
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
545
1036
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
546
1037
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1038
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
547
1039
  multi: multi ?? undefined,
548
1040
  providedIn: injectable?.providedIn,
549
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1041
+ hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
550
1042
  exported: exportsSet.has(token),
551
1043
  file,
552
1044
  line,
553
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1045
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
554
1046
  };
555
1047
  }
556
1048
  if (useValueExpr) {
1049
+ validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
557
1050
  return {
558
1051
  token,
559
1052
  tokenKind,
560
1053
  kind: "value",
561
- useValueExpr: useValueExpr.getText(),
1054
+ useValueExpr: nodeText(useValueExpr),
562
1055
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
563
1056
  deps: [],
564
1057
  multi: multi ?? undefined,
565
1058
  exported: exportsSet.has(token),
566
1059
  file,
567
1060
  line,
568
- importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
1061
+ importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
569
1062
  };
570
1063
  }
571
1064
  if (useFactoryExpr) {
572
- const factoryName = Node.isIdentifier(useFactoryExpr) ? (() => {
573
- const decl = resolveDeclaration(useFactoryExpr)[0];
574
- return decl && (Node.isFunctionDeclaration(decl) || Node.isVariableDeclaration(decl)) ? decl.getName() ?? useFactoryExpr.getText() : useFactoryExpr.getText();
575
- })() : useFactoryExpr.getText();
1065
+ const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
1066
+ const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
1067
+ return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
1068
+ })() : nodeText(useFactoryExpr);
1069
+ validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
576
1070
  return {
577
1071
  token,
578
1072
  tokenKind,
@@ -584,11 +1078,12 @@ function parseProvider(el, exportsSet, ctx) {
584
1078
  exported: exportsSet.has(token),
585
1079
  file,
586
1080
  line,
587
- importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
1081
+ importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
588
1082
  };
589
1083
  }
590
1084
  if (useExistingExpr) {
591
1085
  const target = tokenNameOf(useExistingExpr, ctx).name;
1086
+ validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
592
1087
  return {
593
1088
  token,
594
1089
  tokenKind,
@@ -604,14 +1099,262 @@ function parseProvider(el, exportsSet, ctx) {
604
1099
  }
605
1100
  return;
606
1101
  }
1102
+ function expandProviderExpressions(expressions, ctx, seen = new Set) {
1103
+ const result = [];
1104
+ for (const expression of expressions) {
1105
+ if (ts3.isSpreadElement(expression)) {
1106
+ result.push(...expandProviderExpressions([expression.expression], ctx, seen));
1107
+ continue;
1108
+ }
1109
+ if (ts3.isIdentifier(expression)) {
1110
+ const declaration = resolveDeclaration(expression, ctx)[0];
1111
+ if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
1112
+ const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
1113
+ if (seen.has(key))
1114
+ continue;
1115
+ const initializer = declaration.initializer;
1116
+ if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
1117
+ const nested = initializer.arguments[0];
1118
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1119
+ seen.add(key);
1120
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1121
+ seen.delete(key);
1122
+ continue;
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
1128
+ const nested = expression.arguments[0];
1129
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1130
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1131
+ continue;
1132
+ }
1133
+ }
1134
+ result.push(expression);
1135
+ }
1136
+ return result;
1137
+ }
1138
+ function isProviderHelper(expression, name) {
1139
+ return nodeText(expression.expression).split(".").pop() === name;
1140
+ }
1141
+ function parseFunctionalProvider(expression, exportsSet, ctx) {
1142
+ if (!ts3.isCallExpression(expression))
1143
+ return;
1144
+ const helper = nodeText(expression.expression).split(".").pop();
1145
+ const args = expression.arguments;
1146
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1147
+ const line = lineOf(expression);
1148
+ if (helper === "provideToken") {
1149
+ const tokenExpr = args[0];
1150
+ const valueExpr = args[1];
1151
+ if (!tokenExpr || !valueExpr)
1152
+ return [];
1153
+ const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
1154
+ validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
1155
+ return [{
1156
+ token,
1157
+ tokenKind,
1158
+ kind: "value",
1159
+ useValueExpr: nodeText(valueExpr),
1160
+ scope: resolveScope({ tokenName: token }, ctx),
1161
+ deps: [],
1162
+ exported: exportsSet.has(token),
1163
+ file,
1164
+ line,
1165
+ importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
1166
+ }];
1167
+ }
1168
+ if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
1169
+ const initializer = args[0];
1170
+ if (!initializer)
1171
+ return [];
1172
+ const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
1173
+ return [{
1174
+ token,
1175
+ tokenKind: "injection-token",
1176
+ kind: "value",
1177
+ useValueExpr: nodeText(initializer),
1178
+ scope: "application",
1179
+ deps: [],
1180
+ multi: true,
1181
+ exported: false,
1182
+ file,
1183
+ line,
1184
+ importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
1185
+ }];
1186
+ }
1187
+ if (helper === "provideRouter") {
1188
+ const providers = [];
1189
+ const routes = args[0];
1190
+ if (routes) {
1191
+ providers.push({
1192
+ token: "ROUTE_CONFIG",
1193
+ tokenKind: "injection-token",
1194
+ kind: "value",
1195
+ useValueExpr: nodeText(routes),
1196
+ scope: "application",
1197
+ deps: [],
1198
+ exported: false,
1199
+ file,
1200
+ line,
1201
+ importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
1202
+ });
1203
+ }
1204
+ for (const feature of args.slice(1)) {
1205
+ if (!ts3.isCallExpression(feature))
1206
+ continue;
1207
+ const featureName = nodeText(feature.expression).split(".").pop();
1208
+ if (featureName === "withRouterConfig" && feature.arguments[0]) {
1209
+ providers.push({
1210
+ token: "ROUTER_CONFIGURATION",
1211
+ tokenKind: "injection-token",
1212
+ kind: "value",
1213
+ useValueExpr: nodeText(feature.arguments[0]),
1214
+ scope: "application",
1215
+ deps: [],
1216
+ exported: false,
1217
+ file,
1218
+ line
1219
+ });
1220
+ } else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
1221
+ const strategy = feature.arguments[0];
1222
+ const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
1223
+ providers.push({
1224
+ token: "TITLE_STRATEGY",
1225
+ tokenKind: "injection-token",
1226
+ kind: isClass ? "class" : "value",
1227
+ ...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
1228
+ scope: "application",
1229
+ deps: [],
1230
+ exported: false,
1231
+ file,
1232
+ line,
1233
+ importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
1234
+ });
1235
+ }
1236
+ }
1237
+ return providers;
1238
+ }
1239
+ if (helper === "provideHttpClient") {
1240
+ const providers = [{
1241
+ token: "HttpClient",
1242
+ tokenKind: "class",
1243
+ kind: "class",
1244
+ useClass: "HttpClient",
1245
+ scope: "application",
1246
+ deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1247
+ optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1248
+ exported: false,
1249
+ file,
1250
+ line,
1251
+ importModule: "@supacloud/app"
1252
+ }];
1253
+ for (const feature of args) {
1254
+ if (!ts3.isCallExpression(feature))
1255
+ continue;
1256
+ const featureName = nodeText(feature.expression).split(".").pop();
1257
+ if (featureName === "withInterceptors") {
1258
+ for (const interceptorArg of feature.arguments) {
1259
+ const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
1260
+ for (const value of values) {
1261
+ providers.push({
1262
+ token: "HTTP_INTERCEPTORS",
1263
+ tokenKind: "injection-token",
1264
+ kind: "value",
1265
+ useValueExpr: nodeText(value),
1266
+ scope: "application",
1267
+ deps: [],
1268
+ multi: true,
1269
+ exported: false,
1270
+ file,
1271
+ line,
1272
+ importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
1273
+ });
1274
+ }
1275
+ }
1276
+ } else if (featureName === "withFetch" && feature.arguments.length > 0) {
1277
+ warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
1278
+ }
1279
+ }
1280
+ return providers;
1281
+ }
1282
+ return;
1283
+ }
1284
+ function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
1285
+ const expected = providerTokenValueType(provideExpr, ctx);
1286
+ const actual = providerImplementationType(implementationExpr, kind, ctx);
1287
+ if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
1288
+ return;
1289
+ if (ctx.checker.isTypeAssignableTo(actual, expected))
1290
+ return;
1291
+ const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
1292
+ ctx.diagnostics.push({
1293
+ severity: "error",
1294
+ code: "provider-type-mismatch",
1295
+ message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
1296
+ file,
1297
+ line,
1298
+ errorCode: "SC2010",
1299
+ docsUrl: "https://supacloud.dev/errors/SC2010"
1300
+ });
1301
+ }
1302
+ function providerTokenValueType(expr, ctx) {
1303
+ const type = ctx.checker.getTypeAtLocation(expr);
1304
+ const typeArguments = typeArgumentsOf(type, ctx);
1305
+ if (typeArguments.length > 0)
1306
+ return typeArguments[0];
1307
+ if (ts3.isIdentifier(expr)) {
1308
+ const declaration = resolveDeclaration(expr, ctx)[0];
1309
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1310
+ return declaredClassType(declaration, ctx);
1311
+ }
1312
+ }
1313
+ return;
1314
+ }
1315
+ function providerImplementationType(expr, kind, ctx) {
1316
+ if (kind === "class" || kind === "existing") {
1317
+ if (ts3.isIdentifier(expr)) {
1318
+ const declaration = resolveDeclaration(expr, ctx)[0];
1319
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1320
+ return declaredClassType(declaration, ctx);
1321
+ }
1322
+ }
1323
+ const type = ctx.checker.getTypeAtLocation(expr);
1324
+ const typeArguments = typeArgumentsOf(type, ctx);
1325
+ return typeArguments.length > 0 ? typeArguments[0] : undefined;
1326
+ }
1327
+ if (kind === "factory") {
1328
+ const type = ctx.checker.getTypeAtLocation(expr);
1329
+ const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
1330
+ return signature?.getReturnType();
1331
+ }
1332
+ return ctx.checker.getTypeAtLocation(expr);
1333
+ }
1334
+ function declaredClassType(declaration, ctx) {
1335
+ const name = declaration.name;
1336
+ if (!name)
1337
+ return;
1338
+ const symbol = ctx.checker.getSymbolAtLocation(name);
1339
+ return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
1340
+ }
1341
+ function typeArgumentsOf(type, ctx) {
1342
+ return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
1343
+ }
1344
+ function isTypeReference(type) {
1345
+ return "target" in type;
1346
+ }
1347
+ function isUnknownOrAny(type) {
1348
+ return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
1349
+ }
607
1350
  function parseController(input, ctx) {
608
1351
  let decl;
609
- if (Node.isClassDeclaration(input)) {
1352
+ if (ts3.isClassDeclaration(input)) {
610
1353
  decl = input;
611
1354
  } else {
612
1355
  const unwrapped = unwrapForwardRef(input);
613
- const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
614
- if (resolved && Node.isClassDeclaration(resolved)) {
1356
+ const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
1357
+ if (resolved && ts3.isClassDeclaration(resolved)) {
615
1358
  decl = resolved;
616
1359
  }
617
1360
  }
@@ -622,46 +1365,46 @@ function parseController(input, ctx) {
622
1365
  return;
623
1366
  let path = "/";
624
1367
  let standalone;
625
- const pathArg = controllerDec.getArguments()[0];
1368
+ const pathArg = decoratorArguments(controllerDec)[0];
626
1369
  if (pathArg) {
627
- if (Node.isStringLiteral(pathArg)) {
628
- path = pathArg.getLiteralText();
629
- } else if (Node.isObjectLiteralExpression(pathArg)) {
1370
+ if (ts3.isStringLiteral(pathArg)) {
1371
+ path = pathArg.text;
1372
+ } else if (ts3.isObjectLiteralExpression(pathArg)) {
630
1373
  const p = stringLiteralProp(pathArg, "path");
631
1374
  if (p)
632
1375
  path = p;
633
1376
  standalone = booleanProp(pathArg, "standalone");
634
1377
  }
635
1378
  }
636
- const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
637
- const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1379
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
1380
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
638
1381
  if (missing) {
639
- warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
1382
+ warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
640
1383
  }
641
1384
  const injectable = parseInjectableOptions(decl, ctx);
642
1385
  const routes = [];
643
1386
  const schemaImports = {};
644
1387
  const classGuards = [];
645
- for (const dec of decl.getDecorators()) {
646
- if (decoratorName(dec) === "UseGuards") {
647
- for (const gArg of dec.getArguments()) {
648
- classGuards.push(tokenText(gArg));
1388
+ for (const dec of decoratorsOf(decl)) {
1389
+ if (decoratorName2(dec) === "UseGuards") {
1390
+ for (const gArg of decoratorArguments(dec)) {
1391
+ classGuards.push(tokenText(gArg, ctx));
649
1392
  }
650
1393
  }
651
1394
  }
652
- for (const method of decl.getMethods()) {
653
- for (const dec of method.getDecorators()) {
654
- const name = decoratorName(dec);
1395
+ for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
1396
+ for (const dec of decoratorsOf(method)) {
1397
+ const name = decoratorName2(dec);
655
1398
  const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
656
1399
  if (!httpMethod)
657
1400
  continue;
658
- const args = dec.getArguments();
1401
+ const args = decoratorArguments(dec);
659
1402
  const pathArg2 = args[0];
660
- const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
1403
+ const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
661
1404
  const route = {
662
1405
  method: httpMethod,
663
1406
  path: routePath,
664
- handler: method.getName()
1407
+ handler: propertyName(method.name)
665
1408
  };
666
1409
  const pathParams = [];
667
1410
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
@@ -679,13 +1422,13 @@ function parseController(input, ctx) {
679
1422
  const queryDefaults = {};
680
1423
  let hasBodyBinding = false;
681
1424
  const handlerParams = [];
682
- for (const p of method.getParameters()) {
683
- const pName = p.getName();
1425
+ for (const p of method.parameters) {
1426
+ const pName = parameterName(p);
684
1427
  let hasBindingDecorator = false;
685
1428
  let paramNode;
686
- for (const pDec of p.getDecorators()) {
687
- const dName = decoratorName(pDec);
688
- const dArgs = pDec.getArguments();
1429
+ for (const pDec of decoratorsOf(p)) {
1430
+ const dName = decoratorName2(pDec);
1431
+ const dArgs = decoratorArguments(pDec);
689
1432
  if (dName === "Param") {
690
1433
  hasBindingDecorator = true;
691
1434
  const parsed = parseBindingOptions(dArgs, pName);
@@ -727,7 +1470,7 @@ function parseController(input, ctx) {
727
1470
  }
728
1471
  if (!hasBindingDecorator && pathParams.includes(pName)) {
729
1472
  paramBindings.push(pName);
730
- const typeText = p.getType().getText();
1473
+ const typeText = p.type ? nodeText(p.type) : "";
731
1474
  let inferredTransform;
732
1475
  if (typeText === "number") {
733
1476
  paramTransforms[pName] = "number";
@@ -770,37 +1513,37 @@ function parseController(input, ctx) {
770
1513
  route.handlerParams = handlerParams;
771
1514
  const routeGuards = [...classGuards];
772
1515
  const routeCanDeactivate = [];
773
- for (const mDec of method.getDecorators()) {
774
- const dName = decoratorName(mDec);
775
- const mArgs = mDec.getArguments();
1516
+ for (const mDec of decoratorsOf(method)) {
1517
+ const dName = decoratorName2(mDec);
1518
+ const mArgs = decoratorArguments(mDec);
776
1519
  if (dName === "UseGuards") {
777
1520
  for (const gArg of mArgs) {
778
- routeGuards.push(tokenText(gArg));
1521
+ routeGuards.push(tokenText(gArg, ctx));
779
1522
  }
780
1523
  } else if (dName === "CanDeactivate") {
781
1524
  for (const gArg of mArgs) {
782
- routeCanDeactivate.push(tokenText(gArg));
1525
+ routeCanDeactivate.push(tokenText(gArg, ctx));
783
1526
  }
784
1527
  } else if (dName === "Title") {
785
1528
  const tArg = mArgs[0];
786
- if (tArg && Node.isStringLiteral(tArg)) {
787
- route.title = tArg.getLiteralText();
1529
+ if (tArg && ts3.isStringLiteral(tArg)) {
1530
+ route.title = tArg.text;
788
1531
  }
789
1532
  } else if (dName === "Data") {
790
1533
  const dArg = mArgs[0];
791
- if (dArg && Node.isObjectLiteralExpression(dArg)) {
1534
+ if (dArg && ts3.isObjectLiteralExpression(dArg)) {
792
1535
  route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
793
1536
  }
794
1537
  } else if (dName === "Resolve") {
795
1538
  const rArg = mArgs[0];
796
- if (rArg && Node.isObjectLiteralExpression(rArg)) {
1539
+ if (rArg && ts3.isObjectLiteralExpression(rArg)) {
797
1540
  const resolvers = route.resolvers ?? {};
798
- for (const prop of rArg.getProperties()) {
799
- if (Node.isPropertyAssignment(prop)) {
800
- const rName = prop.getName();
801
- const init = prop.getInitializer();
1541
+ for (const prop of rArg.properties) {
1542
+ if (ts3.isPropertyAssignment(prop)) {
1543
+ const rName = propertyName(prop.name);
1544
+ const init = prop.initializer;
802
1545
  if (init)
803
- resolvers[rName] = tokenText(init);
1546
+ resolvers[rName] = tokenText(init, ctx);
804
1547
  }
805
1548
  }
806
1549
  if (Object.keys(resolvers).length > 0) {
@@ -810,52 +1553,52 @@ function parseController(input, ctx) {
810
1553
  }
811
1554
  }
812
1555
  const optionsArg = args[1];
813
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
1556
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
814
1557
  for (const field of ["body", "params", "query", "response"]) {
815
1558
  const schemaExpr = getProp(optionsArg, field);
816
- if (schemaExpr && Node.isIdentifier(schemaExpr)) {
817
- route[field] = schemaExpr.getText();
1559
+ if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
1560
+ route[field] = nodeText(schemaExpr);
818
1561
  const importPath = importPathOf(schemaExpr, ctx);
819
1562
  if (importPath)
820
- schemaImports[schemaExpr.getText()] = importPath;
1563
+ schemaImports[schemaExpr.text] = importPath;
821
1564
  }
822
1565
  }
823
1566
  const commandExpr = getProp(optionsArg, "command");
824
- if (commandExpr && Node.isIdentifier(commandExpr)) {
825
- const commandDecl = resolveDeclaration(commandExpr)[0];
826
- route.command = commandDecl && Node.isClassDeclaration(commandDecl) ? commandDecl.getName() ?? commandExpr.getText() : commandExpr.getText();
1567
+ if (commandExpr && ts3.isIdentifier(commandExpr)) {
1568
+ const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
1569
+ route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
827
1570
  }
828
1571
  const guardsExpr = getProp(optionsArg, "guards");
829
- if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
830
- for (const el of guardsExpr.getElements()) {
831
- routeGuards.push(tokenText(el));
1572
+ if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
1573
+ for (const el of guardsExpr.elements) {
1574
+ routeGuards.push(tokenText(el, ctx));
832
1575
  }
833
1576
  }
834
1577
  const canMatchExpr = getProp(optionsArg, "canMatch");
835
- if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
1578
+ if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
836
1579
  const canMatchList = [];
837
- for (const el of canMatchExpr.getElements()) {
838
- canMatchList.push(tokenText(el));
1580
+ for (const el of canMatchExpr.elements) {
1581
+ canMatchList.push(tokenText(el, ctx));
839
1582
  }
840
1583
  if (canMatchList.length > 0) {
841
1584
  route.canMatch = canMatchList;
842
1585
  }
843
1586
  }
844
1587
  const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
845
- if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
846
- for (const el of canDeactivateExpr.getElements()) {
847
- routeCanDeactivate.push(tokenText(el));
1588
+ if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
1589
+ for (const el of canDeactivateExpr.elements) {
1590
+ routeCanDeactivate.push(tokenText(el, ctx));
848
1591
  }
849
1592
  }
850
1593
  const resolversExpr = getProp(optionsArg, "resolvers");
851
- if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
1594
+ if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
852
1595
  const resolvers = {};
853
- for (const prop of resolversExpr.getProperties()) {
854
- if (Node.isPropertyAssignment(prop)) {
855
- const rName = prop.getName();
856
- const init = prop.getInitializer();
1596
+ for (const prop of resolversExpr.properties) {
1597
+ if (ts3.isPropertyAssignment(prop)) {
1598
+ const rName = propertyName(prop.name);
1599
+ const init = prop.initializer;
857
1600
  if (init)
858
- resolvers[rName] = tokenText(init);
1601
+ resolvers[rName] = tokenText(init, ctx);
859
1602
  }
860
1603
  }
861
1604
  if (Object.keys(resolvers).length > 0) {
@@ -863,24 +1606,27 @@ function parseController(input, ctx) {
863
1606
  }
864
1607
  }
865
1608
  const redirectToExpr = getProp(optionsArg, "redirectTo");
866
- if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
867
- route.redirectTo = redirectToExpr.getLiteralText();
1609
+ if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
1610
+ route.redirectTo = redirectToExpr.text;
868
1611
  }
869
1612
  const pathMatchExpr = getProp(optionsArg, "pathMatch");
870
- if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
871
- const val = pathMatchExpr.getLiteralText();
1613
+ if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
1614
+ const val = pathMatchExpr.text;
872
1615
  if (val === "full" || val === "prefix") {
873
1616
  route.pathMatch = val;
874
1617
  }
875
1618
  }
876
1619
  const titleExpr = getProp(optionsArg, "title");
877
- if (titleExpr && Node.isStringLiteral(titleExpr)) {
878
- route.title = titleExpr.getLiteralText();
1620
+ if (titleExpr && ts3.isStringLiteral(titleExpr)) {
1621
+ route.title = titleExpr.text;
879
1622
  }
880
1623
  const dataExpr = getProp(optionsArg, "data");
881
- if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
1624
+ if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
882
1625
  route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
883
1626
  }
1627
+ const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
1628
+ if (aspects.length > 0)
1629
+ route.aspects = aspects;
884
1630
  }
885
1631
  if (routeGuards.length > 0) {
886
1632
  route.guards = routeGuards;
@@ -892,46 +1638,40 @@ function parseController(input, ctx) {
892
1638
  }
893
1639
  }
894
1640
  return {
895
- className: decl.getName() ?? "<anonymous>",
1641
+ className: decl.name?.text ?? "<anonymous>",
896
1642
  path,
897
1643
  scope: injectable?.scope ?? "request",
898
1644
  deps,
1645
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
899
1646
  optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
900
1647
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
901
1648
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1649
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1650
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
902
1651
  standalone: standalone || undefined,
903
1652
  routes,
904
1653
  file,
905
- importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
1654
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
906
1655
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
907
1656
  };
908
1657
  }
909
1658
  function classDeps(cls, ctx) {
910
1659
  const injectable = parseInjectableOptions(cls, ctx);
911
- if (injectable?.deps) {
912
- return {
913
- deps: injectable.deps,
914
- optionalDeps: [],
915
- selfDeps: [],
916
- skipSelfDeps: [],
917
- hostDeps: [],
918
- missing: false
919
- };
920
- }
921
- const ctor = cls.getConstructors()[0];
922
- const deps = [];
1660
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1661
+ const deps = injectable?.deps ? [...injectable.deps] : [];
923
1662
  const optionalDeps = [];
924
1663
  const selfDeps = [];
925
1664
  const skipSelfDeps = [];
926
1665
  const hostDeps = [];
1666
+ const functionalInjects = [];
927
1667
  let missing = false;
928
- if (ctor && ctor.getParameters().length > 0) {
929
- const injectParams = parseInjectParams(cls);
1668
+ if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
1669
+ const injectParams = parseInjectParams(cls, ctx);
930
1670
  const optionalIndices = parseOptionalParams(cls);
931
1671
  const selfIndices = parseModifierParams(cls, "Self");
932
1672
  const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
933
1673
  const hostIndices = parseModifierParams(cls, "Host");
934
- ctor.getParameters().forEach((param, index) => {
1674
+ ctor.parameters.forEach((param, index) => {
935
1675
  const isOptional = optionalIndices.has(index);
936
1676
  const injected = injectParams.get(index);
937
1677
  const tokenName = injected ?? paramTypeTokenName(param, ctx);
@@ -951,47 +1691,62 @@ function classDeps(cls, ctx) {
951
1691
  }
952
1692
  });
953
1693
  }
954
- for (const prop of cls.getProperties()) {
955
- const init = prop.getInitializer();
956
- if (init && Node.isCallExpression(init)) {
957
- const callName = init.getExpression().getText().split(".").pop();
1694
+ for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
1695
+ const init = prop.initializer;
1696
+ if (init && ts3.isCallExpression(init)) {
1697
+ const callName = nodeText(init.expression).split(".").pop();
958
1698
  if (callName === "inject") {
959
- const [tokenArg, optionsArg] = init.getArguments();
1699
+ const [tokenArg, optionsArg] = init.arguments;
960
1700
  if (tokenArg) {
961
- const tokenName = tokenText(tokenArg);
962
- if (tokenName) {
963
- if (!deps.includes(tokenName))
964
- deps.push(tokenName);
965
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
966
- const isOptional = booleanProp(optionsArg, "optional");
967
- if (isOptional && !optionalDeps.includes(tokenName)) {
968
- optionalDeps.push(tokenName);
969
- }
970
- const isSelf = booleanProp(optionsArg, "self");
971
- if (isSelf && !selfDeps.includes(tokenName)) {
972
- selfDeps.push(tokenName);
973
- }
974
- const isSkipSelf = booleanProp(optionsArg, "skipSelf");
975
- if (isSkipSelf && !skipSelfDeps.includes(tokenName)) {
976
- skipSelfDeps.push(tokenName);
977
- }
978
- const isHost = booleanProp(optionsArg, "host");
979
- if (isHost && !hostDeps.includes(tokenName)) {
980
- hostDeps.push(tokenName);
981
- }
982
- }
1701
+ const tokenName = tokenText(tokenArg, ctx);
1702
+ const unwrappedToken = unwrapForwardRef(tokenArg);
1703
+ const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
1704
+ if (!known) {
1705
+ missing = true;
1706
+ continue;
1707
+ }
1708
+ if (!deps.includes(tokenName))
1709
+ deps.push(tokenName);
1710
+ const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
1711
+ optional: booleanProp(optionsArg, "optional") ?? false,
1712
+ self: booleanProp(optionsArg, "self") ?? false,
1713
+ skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
1714
+ host: booleanProp(optionsArg, "host") ?? false
1715
+ } : { optional: false, self: false, skipSelf: false, host: false };
1716
+ if (options.optional && !optionalDeps.includes(tokenName))
1717
+ optionalDeps.push(tokenName);
1718
+ if (options.self && !selfDeps.includes(tokenName))
1719
+ selfDeps.push(tokenName);
1720
+ if (options.skipSelf && !skipSelfDeps.includes(tokenName))
1721
+ skipSelfDeps.push(tokenName);
1722
+ if (options.host && !hostDeps.includes(tokenName))
1723
+ hostDeps.push(tokenName);
1724
+ if (!functionalInjects.some((entry) => entry.token === tokenName)) {
1725
+ functionalInjects.push({
1726
+ token: tokenName,
1727
+ expression: nodeText(unwrappedToken),
1728
+ importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
1729
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1730
+ return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
1731
+ })() : undefined,
1732
+ importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
1733
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1734
+ return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
1735
+ })() : undefined,
1736
+ ...options
1737
+ });
983
1738
  }
984
1739
  }
985
1740
  }
986
1741
  }
987
1742
  }
988
- return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
1743
+ return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
989
1744
  }
990
1745
  function paramTypeTokenName(param, ctx) {
991
- const typeNode = param.getTypeNode();
1746
+ const typeNode = param.type;
992
1747
  if (!typeNode)
993
1748
  return;
994
- const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
1749
+ const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
995
1750
  if (ctx.classesByName.has(text))
996
1751
  return text;
997
1752
  if (ctx.tokensByName.has(text))
@@ -1009,63 +1764,63 @@ function parseInjectableOptions(cls, ctx) {
1009
1764
  const providedIn = stringLiteralProp(obj, "providedIn");
1010
1765
  const depsExpr = getProp(obj, "deps");
1011
1766
  return {
1012
- scope: scope && SCOPES.includes(scope) ? scope : undefined,
1767
+ scope: scope && isScope(scope) ? scope : undefined,
1013
1768
  providedIn: providedIn === "root" ? "root" : undefined,
1014
- deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
1769
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
1015
1770
  };
1016
1771
  }
1017
- function parseInjectParams(cls) {
1772
+ function parseInjectParams(cls, ctx) {
1018
1773
  const result = new Map;
1019
- const ctor = cls.getConstructors()[0];
1774
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1020
1775
  if (!ctor)
1021
1776
  return result;
1022
- ctor.getParameters().forEach((param, index) => {
1023
- for (const dec of param.getDecorators()) {
1024
- if (decoratorName(dec) !== "Inject")
1777
+ ctor.parameters.forEach((param, index) => {
1778
+ for (const dec of decoratorsOf(param)) {
1779
+ if (decoratorName2(dec) !== "Inject")
1025
1780
  continue;
1026
- const arg = dec.getArguments()[0];
1781
+ const arg = decoratorArguments(dec)[0];
1027
1782
  if (arg)
1028
- result.set(index, tokenText(arg));
1783
+ result.set(index, tokenText(arg, ctx));
1029
1784
  }
1030
1785
  });
1031
1786
  return result;
1032
1787
  }
1033
1788
  function parseOptionalParams(cls) {
1034
1789
  const result = new Set;
1035
- const ctor = cls.getConstructors()[0];
1790
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1036
1791
  if (!ctor)
1037
1792
  return result;
1038
- ctor.getParameters().forEach((param, index) => {
1039
- for (const dec of param.getDecorators()) {
1040
- if (decoratorName(dec) === "Optional")
1793
+ ctor.parameters.forEach((param, index) => {
1794
+ for (const dec of decoratorsOf(param)) {
1795
+ if (decoratorName2(dec) === "Optional")
1041
1796
  result.add(index);
1042
1797
  }
1043
- if (param.hasQuestionToken())
1798
+ if (param.questionToken)
1044
1799
  result.add(index);
1045
1800
  });
1046
1801
  return result;
1047
1802
  }
1048
1803
  function parseModifierParams(cls, modifierName) {
1049
1804
  const result = new Set;
1050
- const ctor = cls.getConstructors()[0];
1805
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1051
1806
  if (!ctor)
1052
1807
  return result;
1053
- ctor.getParameters().forEach((param, index) => {
1054
- for (const dec of param.getDecorators()) {
1055
- if (decoratorName(dec) === modifierName)
1808
+ ctor.parameters.forEach((param, index) => {
1809
+ for (const dec of decoratorsOf(param)) {
1810
+ if (decoratorName2(dec) === modifierName)
1056
1811
  result.add(index);
1057
1812
  }
1058
1813
  });
1059
1814
  return result;
1060
1815
  }
1061
1816
  function unwrapForwardRef(expr) {
1062
- if (Node.isCallExpression(expr)) {
1063
- const exprText = expr.getExpression().getText();
1817
+ if (ts3.isCallExpression(expr)) {
1818
+ const exprText = nodeText(expr.expression);
1064
1819
  if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
1065
- const arg = expr.getArguments()[0];
1066
- if (arg && (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg))) {
1067
- const body = arg.getBody();
1068
- if (body && Node.isExpression(body)) {
1820
+ const arg = expr.arguments[0];
1821
+ if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
1822
+ const body = arg.body;
1823
+ if (body && ts3.isExpression(body)) {
1069
1824
  return unwrapForwardRef(body);
1070
1825
  }
1071
1826
  }
@@ -1073,18 +1828,18 @@ function unwrapForwardRef(expr) {
1073
1828
  }
1074
1829
  return expr;
1075
1830
  }
1076
- function tokenText(expr) {
1831
+ function tokenText(expr, ctx) {
1077
1832
  const unwrapped = unwrapForwardRef(expr);
1078
- if (Node.isStringLiteral(unwrapped))
1079
- return unwrapped.getLiteralText();
1080
- if (Node.isIdentifier(unwrapped)) {
1081
- const decl = resolveDeclaration(unwrapped)[0];
1082
- if (decl && Node.isClassDeclaration(decl))
1083
- return decl.getName() ?? unwrapped.getText();
1084
- if (decl && Node.isVariableDeclaration(decl))
1085
- return decl.getName();
1833
+ if (ts3.isStringLiteral(unwrapped))
1834
+ return unwrapped.text;
1835
+ if (ts3.isIdentifier(unwrapped)) {
1836
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1837
+ if (decl && ts3.isClassDeclaration(decl))
1838
+ return decl.name?.text ?? unwrapped.text;
1839
+ if (decl && ts3.isVariableDeclaration(decl))
1840
+ return variableName(decl);
1086
1841
  }
1087
- return unwrapped.getText();
1842
+ return nodeText(unwrapped);
1088
1843
  }
1089
1844
  function resolveScope(input, ctx) {
1090
1845
  if (input.explicit)
@@ -1101,98 +1856,181 @@ function resolveScope(input, ctx) {
1101
1856
  }
1102
1857
  function tokenNameOf(expr, ctx) {
1103
1858
  const unwrapped = unwrapForwardRef(expr);
1104
- if (Node.isIdentifier(unwrapped)) {
1105
- const decl = resolveDeclaration(unwrapped)[0];
1106
- if (decl && Node.isClassDeclaration(decl)) {
1107
- return { name: decl.getName() ?? expr.getText(), kind: "class" };
1859
+ if (ts3.isIdentifier(unwrapped)) {
1860
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1861
+ if (decl && ts3.isClassDeclaration(decl)) {
1862
+ return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
1108
1863
  }
1109
- if (decl && Node.isVariableDeclaration(decl)) {
1110
- const name = decl.getName();
1864
+ if (decl && ts3.isVariableDeclaration(decl)) {
1865
+ const name = variableName(decl);
1111
1866
  return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
1112
1867
  }
1113
- if (ctx.tokensByName.has(expr.getText())) {
1114
- return { name: expr.getText(), kind: "injection-token" };
1868
+ if (ctx.tokensByName.has(unwrapped.text)) {
1869
+ return { name: unwrapped.text, kind: "injection-token" };
1115
1870
  }
1116
1871
  }
1117
- return { name: expr.getText(), kind: "class" };
1872
+ return { name: nodeText(expr), kind: "class" };
1118
1873
  }
1119
- function resolveDeclaration(id) {
1120
- let symbol = id.getSymbol();
1874
+ function resolveDeclaration(id, ctx) {
1875
+ let symbol = ctx.checker.getSymbolAtLocation(id);
1121
1876
  if (!symbol)
1122
1877
  return [];
1123
- let declarations = symbol.getDeclarations();
1878
+ let declarations = symbol.declarations ?? [];
1124
1879
  for (let guard = 0;guard < 4; guard += 1) {
1125
- const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
1880
+ const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
1126
1881
  if (!isAlias)
1127
1882
  break;
1128
- const aliased = symbol.getAliasedSymbol();
1129
- if (!aliased)
1883
+ if (!(symbol.flags & ts3.SymbolFlags.Alias))
1130
1884
  break;
1885
+ const aliased = ctx.checker.getAliasedSymbol(symbol);
1131
1886
  symbol = aliased;
1132
- declarations = aliased.getDeclarations();
1887
+ declarations = aliased.declarations ?? [];
1133
1888
  }
1134
1889
  return declarations;
1135
1890
  }
1136
1891
  function importPathOf(id, ctx) {
1137
- const symbol = id.getSymbol();
1138
- const first = symbol?.getDeclarations()[0];
1139
- if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
1140
- const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
1141
- const target = importDecl?.getModuleSpecifierSourceFile();
1142
- if (target)
1143
- return modulePath(ctx.rootDir, target.getFilePath());
1144
- }
1145
- const decl = resolveDeclaration(id)[0];
1892
+ const decl = resolveDeclaration(id, ctx)[0];
1146
1893
  if (decl)
1147
- return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1894
+ return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
1895
+ return;
1896
+ }
1897
+ function importModuleOf(id, ctx) {
1898
+ const symbol = ctx.checker.getSymbolAtLocation(id);
1899
+ const declarations = symbol?.declarations ?? [];
1900
+ for (const declaration of declarations) {
1901
+ let current = declaration;
1902
+ while (current) {
1903
+ if (ts3.isImportDeclaration(current)) {
1904
+ const moduleSpecifier = current.moduleSpecifier;
1905
+ return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
1906
+ }
1907
+ current = current.parent;
1908
+ }
1909
+ }
1148
1910
  return;
1149
1911
  }
1150
1912
  function findDecorator(cls, name) {
1151
- return cls.getDecorators().find((dec) => decoratorName(dec) === name);
1913
+ return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
1152
1914
  }
1153
- function decoratorName(dec) {
1154
- const expr = dec.getExpression();
1155
- if (Node.isCallExpression(expr)) {
1156
- return expr.getExpression().getText().split(".").pop();
1915
+ function decoratorName2(dec) {
1916
+ const expr = dec.expression;
1917
+ if (ts3.isCallExpression(expr)) {
1918
+ return nodeText(expr.expression).split(".").pop();
1157
1919
  }
1158
- if (Node.isIdentifier(expr))
1159
- return expr.getText();
1920
+ if (ts3.isIdentifier(expr))
1921
+ return expr.text;
1160
1922
  return;
1161
1923
  }
1162
1924
  function decoratorObjectArg(dec) {
1163
- const expr = dec.getExpression();
1164
- if (!Node.isCallExpression(expr))
1925
+ const expr = dec.expression;
1926
+ if (!ts3.isCallExpression(expr))
1165
1927
  return;
1166
- const arg = expr.getArguments()[0];
1167
- return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
1928
+ const arg = expr.arguments[0];
1929
+ return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
1168
1930
  }
1169
1931
  function getProp(obj, name) {
1170
- const prop = obj.getProperty(name);
1171
- if (prop && Node.isPropertyAssignment(prop))
1172
- return prop.getInitializer();
1932
+ const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
1933
+ if (!prop)
1934
+ return;
1935
+ if (ts3.isPropertyAssignment(prop))
1936
+ return prop.initializer;
1937
+ if (ts3.isShorthandPropertyAssignment(prop))
1938
+ return prop.name;
1173
1939
  return;
1174
1940
  }
1941
+ function toCompilerDiagnostic(diagnostic, rootDir) {
1942
+ const file = diagnostic.file;
1943
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
1944
+ return {
1945
+ severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
1946
+ code: `typescript-${diagnostic.code}`,
1947
+ errorCode: `TS${diagnostic.code}`,
1948
+ message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
1949
+ `),
1950
+ file: file ? sourcePath(rootDir, file.fileName) : undefined,
1951
+ line: position ? position.line + 1 : undefined
1952
+ };
1953
+ }
1175
1954
  function stringLiteralProp(obj, name) {
1176
1955
  const expr = getProp(obj, name);
1177
- return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
1956
+ return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
1178
1957
  }
1179
1958
  function arrayProp(obj, name) {
1180
1959
  const expr = getProp(obj, name);
1181
- return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
1960
+ return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
1961
+ }
1962
+ function parseAspectRefs(expression, ctx, owner) {
1963
+ if (!expression)
1964
+ return [];
1965
+ if (!ts3.isArrayLiteralExpression(expression)) {
1966
+ ctx.diagnostics.push({
1967
+ severity: "error",
1968
+ code: "dynamic-aspect-reference",
1969
+ message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
1970
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
1971
+ line: lineOf(expression),
1972
+ suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
1973
+ errorCode: "SC4010",
1974
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1975
+ });
1976
+ return [];
1977
+ }
1978
+ const refs = [];
1979
+ for (const element of expression.elements) {
1980
+ if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
1981
+ ctx.diagnostics.push({
1982
+ severity: "error",
1983
+ code: "dynamic-aspect-reference",
1984
+ message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
1985
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
1986
+ line: lineOf(element),
1987
+ suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
1988
+ errorCode: "SC4010",
1989
+ docsUrl: "https://supacloud.dev/errors/SC4010"
1990
+ });
1991
+ continue;
1992
+ }
1993
+ const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
1994
+ if (!declaration) {
1995
+ ctx.diagnostics.push({
1996
+ severity: "error",
1997
+ code: "invalid-aspect-reference",
1998
+ message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
1999
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
2000
+ line: lineOf(element),
2001
+ suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
2002
+ errorCode: "SC4011",
2003
+ docsUrl: "https://supacloud.dev/errors/SC4011"
2004
+ });
2005
+ continue;
2006
+ }
2007
+ const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
2008
+ if (!name)
2009
+ continue;
2010
+ const declaredFile = declaration.getSourceFile().fileName;
2011
+ const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2012
+ refs.push({
2013
+ name,
2014
+ expression: element.text,
2015
+ importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
2016
+ importModule: projectLocal ? undefined : importModuleOf(element, ctx)
2017
+ });
2018
+ }
2019
+ return refs;
1182
2020
  }
1183
2021
  function booleanProp(obj, name) {
1184
2022
  const expr = getProp(obj, name);
1185
2023
  if (!expr)
1186
2024
  return;
1187
- if (expr.getKind() === SyntaxKind.TrueKeyword)
2025
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
1188
2026
  return true;
1189
- if (expr.getKind() === SyntaxKind.FalseKeyword)
2027
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
1190
2028
  return false;
1191
2029
  return;
1192
2030
  }
1193
2031
  function parseScopeProp(obj) {
1194
2032
  const scope = stringLiteralProp(obj, "scope");
1195
- return scope && SCOPES.includes(scope) ? scope : undefined;
2033
+ return scope && isScope(scope) ? scope : undefined;
1196
2034
  }
1197
2035
  function parseBindingOptions(args, defaultName) {
1198
2036
  let name = defaultName;
@@ -1200,16 +2038,16 @@ function parseBindingOptions(args, defaultName) {
1200
2038
  let defaultValue;
1201
2039
  const first = args[0];
1202
2040
  const second = args[1];
1203
- if (first && Node.isStringLiteral(first)) {
1204
- name = first.getLiteralText();
1205
- } else if (first && Node.isObjectLiteralExpression(first)) {
2041
+ if (first && ts3.isStringLiteral(first)) {
2042
+ name = first.text;
2043
+ } else if (first && ts3.isObjectLiteralExpression(first)) {
1206
2044
  const nameProp = getProp(first, "name");
1207
- if (nameProp && Node.isStringLiteral(nameProp)) {
1208
- name = nameProp.getLiteralText();
2045
+ if (nameProp && ts3.isStringLiteral(nameProp)) {
2046
+ name = nameProp.text;
1209
2047
  }
1210
2048
  const trProp = getProp(first, "transform");
1211
- if (trProp && Node.isStringLiteral(trProp)) {
1212
- const val = trProp.getLiteralText();
2049
+ if (trProp && ts3.isStringLiteral(trProp)) {
2050
+ const val = trProp.text;
1213
2051
  if (val === "number" || val === "boolean" || val === "string") {
1214
2052
  transform = val;
1215
2053
  }
@@ -1219,10 +2057,10 @@ function parseBindingOptions(args, defaultName) {
1219
2057
  defaultValue = parseLiteralValue(defProp);
1220
2058
  }
1221
2059
  }
1222
- if (second && Node.isObjectLiteralExpression(second)) {
2060
+ if (second && ts3.isObjectLiteralExpression(second)) {
1223
2061
  const trProp = getProp(second, "transform");
1224
- if (trProp && Node.isStringLiteral(trProp)) {
1225
- const val = trProp.getLiteralText();
2062
+ if (trProp && ts3.isStringLiteral(trProp)) {
2063
+ const val = trProp.text;
1226
2064
  if (val === "number" || val === "boolean" || val === "string") {
1227
2065
  transform = val;
1228
2066
  }
@@ -1235,28 +2073,28 @@ function parseBindingOptions(args, defaultName) {
1235
2073
  return { name, transform, default: defaultValue };
1236
2074
  }
1237
2075
  function parseLiteralValue(node) {
1238
- if (Node.isStringLiteral(node))
1239
- return node.getLiteralText();
1240
- if (Node.isNumericLiteral(node))
1241
- return node.getLiteralValue();
1242
- if (node.getKindName() === "TrueKeyword")
2076
+ if (ts3.isStringLiteral(node))
2077
+ return node.text;
2078
+ if (ts3.isNumericLiteral(node))
2079
+ return Number(node.text);
2080
+ if (node.kind === ts3.SyntaxKind.TrueKeyword)
1243
2081
  return true;
1244
- if (node.getKindName() === "FalseKeyword")
2082
+ if (node.kind === ts3.SyntaxKind.FalseKeyword)
1245
2083
  return false;
1246
- if (Node.isArrayLiteralExpression(node)) {
1247
- return node.getElements().map(parseLiteralValue);
2084
+ if (ts3.isArrayLiteralExpression(node)) {
2085
+ return node.elements.map(parseLiteralValue);
1248
2086
  }
1249
- if (Node.isObjectLiteralExpression(node)) {
2087
+ if (ts3.isObjectLiteralExpression(node)) {
1250
2088
  return parseObjectLiteralValues(node);
1251
2089
  }
1252
2090
  return;
1253
2091
  }
1254
2092
  function parseObjectLiteralValues(obj) {
1255
2093
  const result = {};
1256
- for (const prop of obj.getProperties()) {
1257
- if (Node.isPropertyAssignment(prop)) {
1258
- const name = prop.getName();
1259
- const init = prop.getInitializer();
2094
+ for (const prop of obj.properties) {
2095
+ if (ts3.isPropertyAssignment(prop)) {
2096
+ const name = propertyName(prop.name);
2097
+ const init = prop.initializer;
1260
2098
  if (init) {
1261
2099
  result[name] = parseLiteralValue(init);
1262
2100
  }
@@ -1275,7 +2113,8 @@ function warn(ctx, code, message, file, line) {
1275
2113
  }
1276
2114
 
1277
2115
  // src/generate.ts
1278
- import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
2116
+ import { createHash as createHash4 } from "node:crypto";
2117
+ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
1279
2118
  import { join as join2 } from "node:path";
1280
2119
 
1281
2120
  // src/util.ts
@@ -1354,6 +2193,7 @@ var INTERFACES = `export interface CompiledRoute {
1354
2193
  queryDefaults?: Record<string, unknown>;
1355
2194
  title?: string;
1356
2195
  data?: Record<string, unknown>;
2196
+ aspects?: CompiledAspect[];
1357
2197
  invoker?: (
1358
2198
  controller: unknown,
1359
2199
  request: {
@@ -1374,8 +2214,33 @@ export interface CompiledCommand {
1374
2214
  audit?: string;
1375
2215
  idempotency: "required" | "none";
1376
2216
  standalone?: boolean;
2217
+ aspects?: CompiledAspect[];
2218
+ }
2219
+
2220
+ export interface CompiledJob {
2221
+ className: string;
2222
+ name: string;
2223
+ serviceKey: string;
2224
+ scope: "application" | "request" | "job";
2225
+ aspects?: CompiledAspect[];
1377
2226
  }
1378
2227
 
2228
+ export interface CompiledAspectContext {
2229
+ kind: "route" | "command" | "job";
2230
+ name: string;
2231
+ input: unknown;
2232
+ request?: Request;
2233
+ requestContext?: unknown;
2234
+ scope?: Record<string, unknown>;
2235
+ services?: Record<string, unknown>;
2236
+ metadata?: unknown;
2237
+ }
2238
+
2239
+ export type CompiledAspect = (
2240
+ context: CompiledAspectContext,
2241
+ next: () => unknown | Promise<unknown>,
2242
+ ) => unknown | Promise<unknown>;
2243
+
1379
2244
  export interface CompiledController {
1380
2245
  path: string;
1381
2246
  serviceKey: string;
@@ -1394,13 +2259,62 @@ export interface CompiledModule {
1394
2259
  ctx: unknown,
1395
2260
  imported?: Record<string, Record<string, unknown>>,
1396
2261
  ): Record<string, unknown>;
2262
+ destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
1397
2263
  createJobScope?(
1398
2264
  services: Record<string, unknown>,
1399
2265
  ctx: unknown,
1400
2266
  imported?: Record<string, Record<string, unknown>>,
1401
2267
  ): Record<string, unknown>;
2268
+ destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
1402
2269
  controllers: CompiledController[];
1403
2270
  commands: CompiledCommand[];
2271
+ jobs: CompiledJob[];
2272
+ aspects?: CompiledAspect[];
2273
+ }`;
2274
+ var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
2275
+ return typeof value === "object" && value !== null;
2276
+ }
2277
+
2278
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2279
+ return typeof value === "function";
2280
+ }
2281
+
2282
+ function resolveFactoryValue(value: unknown): unknown {
2283
+ if (!isRecord(value) || !isFunction(value.factory)) return undefined;
2284
+ return value.factory();
2285
+ }
2286
+
2287
+ const scopeDestructions = new WeakMap<object, Promise<void>>();
2288
+
2289
+ function destroyScopeInstances(
2290
+ scope: Record<string, unknown>,
2291
+ plan: readonly { key: string; index?: number }[],
2292
+ ): Promise<void> {
2293
+ const pending = scopeDestructions.get(scope);
2294
+ if (pending) return pending;
2295
+ const destruction = Promise.resolve().then(async () => {
2296
+ const errors: unknown[] = [];
2297
+ const seen = new Set<unknown>();
2298
+ for (const entry of [...plan].reverse()) {
2299
+ const value = scope[entry.key];
2300
+ const instance = entry.index === undefined ? value
2301
+ : Array.isArray(value) ? value[entry.index] : undefined;
2302
+ if (seen.has(instance)) continue;
2303
+ seen.add(instance);
2304
+ try {
2305
+ if (isRecord(instance) && isFunction(instance.onDestroy)) {
2306
+ await instance.onDestroy();
2307
+ } else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
2308
+ await instance.ngOnDestroy();
2309
+ }
2310
+ } catch (error) {
2311
+ errors.push(error);
2312
+ }
2313
+ }
2314
+ if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
2315
+ });
2316
+ scopeDestructions.set(scope, destruction);
2317
+ return destruction;
1404
2318
  }`;
1405
2319
  function renderApplication(graph, options) {
1406
2320
  let modules = topoSortModules(graph.modules);
@@ -1418,6 +2332,8 @@ function renderApplication(graph, options) {
1418
2332
  referencedTokens.add(d);
1419
2333
  for (const d of ctrl.skipSelfDeps ?? [])
1420
2334
  referencedTokens.add(d);
2335
+ for (const d of ctrl.hostDeps ?? [])
2336
+ referencedTokens.add(d);
1421
2337
  }
1422
2338
  for (const p of mod.providers) {
1423
2339
  for (const d of p.deps ?? [])
@@ -1428,6 +2344,8 @@ function renderApplication(graph, options) {
1428
2344
  referencedTokens.add(d);
1429
2345
  for (const d of p.skipSelfDeps ?? [])
1430
2346
  referencedTokens.add(d);
2347
+ for (const d of p.hostDeps ?? [])
2348
+ referencedTokens.add(d);
1431
2349
  if (p.useExisting)
1432
2350
  referencedTokens.add(p.useExisting);
1433
2351
  }
@@ -1452,6 +2370,8 @@ function renderApplication(graph, options) {
1452
2370
  ...imports.size > 0 ? [""] : [],
1453
2371
  INTERFACES,
1454
2372
  "",
2373
+ TYPE_GUARDS,
2374
+ "",
1455
2375
  "export function createCompiledModules(): CompiledModule[] {",
1456
2376
  " return [",
1457
2377
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
@@ -1459,29 +2379,33 @@ function renderApplication(graph, options) {
1459
2379
  "}",
1460
2380
  "",
1461
2381
  "export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
1462
- ' const initializers = (services.appInitializer ?? (services as any)["supacloud.app-initializer"]) as unknown;',
1463
- " if (Array.isArray(initializers)) {",
1464
- " for (const init of initializers) {",
1465
- ' if (typeof init === "function") await init();',
2382
+ ' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
2383
+ " for (const group of initializers) {",
2384
+ " if (Array.isArray(group)) {",
2385
+ " for (const init of group) {",
2386
+ " if (isFunction(init)) await init();",
2387
+ " }",
2388
+ " } else if (isFunction(group)) {",
2389
+ " await group();",
1466
2390
  " }",
1467
- ' } else if (typeof initializers === "function") {',
1468
- " await (initializers as () => unknown)();",
1469
2391
  " }",
1470
2392
  "}",
1471
2393
  "",
1472
2394
  "export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
1473
- ' const destroyRef = (services.destroyRef ?? (services as any)["supacloud.destroy-ref"]) as { destroy?: () => Promise<void>; _teardowns?: Array<() => void | Promise<void>> } | undefined;',
1474
- ' if (destroyRef && typeof destroyRef.destroy === "function") {',
2395
+ ' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
2396
+ " if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
1475
2397
  " await destroyRef.destroy();",
1476
- " } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
2398
+ " } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
1477
2399
  " for (const teardown of [...destroyRef._teardowns].reverse()) {",
1478
- ' if (typeof teardown === "function") await teardown();',
2400
+ " if (isFunction(teardown)) await teardown();",
1479
2401
  " }",
1480
2402
  " }",
1481
2403
  " const instances = Object.values(services);",
1482
2404
  " for (const inst of instances.reverse()) {",
1483
- ' if (inst && typeof (inst as any).onDestroy === "function") {',
1484
- " await (inst as any).onDestroy();",
2405
+ " if (isRecord(inst) && isFunction(inst.onDestroy)) {",
2406
+ " await inst.onDestroy();",
2407
+ " } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
2408
+ " await inst.ngOnDestroy();",
1485
2409
  " }",
1486
2410
  " }",
1487
2411
  "}",
@@ -1510,21 +2434,39 @@ async function generateApplication(graph, options) {
1510
2434
  await mkdir(options.outDir, { recursive: true });
1511
2435
  const applicationPath = join2(options.outDir, "application.ts");
1512
2436
  const manifestPath = join2(options.outDir, "app.manifest.json");
1513
- await writeFileAtomic(applicationPath, rendered.applicationCode);
1514
- await writeFileAtomic(manifestPath, rendered.manifestJson);
1515
- const written = [applicationPath, manifestPath];
2437
+ const written = [];
2438
+ if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
2439
+ written.push(applicationPath);
2440
+ }
2441
+ if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
2442
+ written.push(manifestPath);
2443
+ }
1516
2444
  if (rendered.clientCode) {
1517
2445
  const clientPath = join2(options.outDir, "client.ts");
1518
- await writeFileAtomic(clientPath, rendered.clientCode);
1519
- written.push(clientPath);
2446
+ if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
2447
+ written.push(clientPath);
2448
+ }
1520
2449
  }
1521
2450
  if (rendered.permissionsCode) {
1522
2451
  const permissionsPath = join2(options.outDir, "permissions.ts");
1523
- await writeFileAtomic(permissionsPath, rendered.permissionsCode);
1524
- written.push(permissionsPath);
2452
+ if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
2453
+ written.push(permissionsPath);
2454
+ }
1525
2455
  }
1526
2456
  return written;
1527
2457
  }
2458
+ async function writeFileIfChanged(path, content, hashes) {
2459
+ const hash = createHash4("sha1").update(content).digest("hex");
2460
+ if (hashes?.get(path) === hash) {
2461
+ try {
2462
+ await access(path);
2463
+ return false;
2464
+ } catch {}
2465
+ }
2466
+ await writeFileAtomic(path, content);
2467
+ hashes?.set(path, hash);
2468
+ return true;
2469
+ }
1528
2470
  async function writeFileAtomic(path, content) {
1529
2471
  const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1530
2472
  try {
@@ -1575,11 +2517,13 @@ class ImportManager {
1575
2517
  get size() {
1576
2518
  return this.entries.size;
1577
2519
  }
1578
- add(exported, importPath) {
1579
- if (!importPath)
2520
+ add(exported, importPath, importModule) {
2521
+ const path = importModule ?? importPath;
2522
+ const packageImport = importModule !== undefined;
2523
+ if (!path)
1580
2524
  return exported;
1581
2525
  for (const [local2, entry] of this.entries) {
1582
- if (entry.path === importPath && entry.exported === exported)
2526
+ if (entry.path === path && entry.exported === exported && entry.package === packageImport)
1583
2527
  return local2;
1584
2528
  }
1585
2529
  let local = exported;
@@ -1588,18 +2532,18 @@ class ImportManager {
1588
2532
  local = `${exported}${counter}`;
1589
2533
  counter += 1;
1590
2534
  }
1591
- this.entries.set(local, { path: importPath, exported });
2535
+ this.entries.set(local, { path, exported, package: packageImport });
1592
2536
  return local;
1593
2537
  }
1594
2538
  render(rootDir, outDir) {
1595
2539
  const byPath = new Map;
1596
2540
  for (const [local, entry] of this.entries) {
1597
- const list = byPath.get(entry.path) ?? [];
2541
+ const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
2542
+ const list = byPath.get(spec) ?? [];
1598
2543
  list.push({ exported: entry.exported, local });
1599
- byPath.set(entry.path, list);
2544
+ byPath.set(spec, list);
1600
2545
  }
1601
- return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
1602
- const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
2546
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
1603
2547
  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(", ");
1604
2548
  return `import { ${names} } from "${spec}";`;
1605
2549
  });
@@ -1621,14 +2565,19 @@ class ModuleGenerator {
1621
2565
  this.module = module;
1622
2566
  this.imports = imports;
1623
2567
  this.pascal = pascalName(module.name);
2568
+ if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
2569
+ imports.add("runInInjectionContext", undefined, "@supacloud/app");
2570
+ }
1624
2571
  }
1625
2572
  renderFactories() {
1626
2573
  const sections = [this.renderServicesFactory()];
1627
2574
  if (this.hasFactoryContent("request")) {
1628
2575
  sections.push(this.renderScopeFactory("request"));
2576
+ sections.push(this.renderScopeDestroyer("request"));
1629
2577
  }
1630
2578
  if (this.hasFactoryContent("job")) {
1631
2579
  sections.push(this.renderScopeFactory("job"));
2580
+ sections.push(this.renderScopeDestroyer("job"));
1632
2581
  }
1633
2582
  return sections;
1634
2583
  }
@@ -1640,12 +2589,18 @@ class ModuleGenerator {
1640
2589
  ];
1641
2590
  if (this.hasFactoryContent("request")) {
1642
2591
  lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
2592
+ lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
1643
2593
  }
1644
2594
  if (this.hasFactoryContent("job")) {
1645
2595
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
2596
+ lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
1646
2597
  }
1647
2598
  lines.push(` controllers: ${this.renderControllers()},`);
1648
- lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
2599
+ lines.push(` commands: ${this.renderCommands()},`);
2600
+ lines.push(` jobs: ${this.renderJobs()},`);
2601
+ if (this.module.aspects && this.module.aspects.length > 0) {
2602
+ lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
2603
+ }
1649
2604
  lines.push(`}`);
1650
2605
  return lines.join(`
1651
2606
  `);
@@ -1708,6 +2663,9 @@ class ModuleGenerator {
1708
2663
  if (route.data && Object.keys(route.data).length > 0) {
1709
2664
  fields.push(`data: ${JSON.stringify(route.data)}`);
1710
2665
  }
2666
+ if (route.aspects && route.aspects.length > 0) {
2667
+ fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2668
+ }
1711
2669
  const invokerArgs = (route.handlerParams ?? []).map((hp) => {
1712
2670
  if (hp.kind === "param") {
1713
2671
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
@@ -1746,7 +2704,7 @@ class ModuleGenerator {
1746
2704
  return "undefined";
1747
2705
  });
1748
2706
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
1749
- fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
2707
+ 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}]); }`);
1750
2708
  return `{ ${fields.join(", ")} }`;
1751
2709
  });
1752
2710
  return [
@@ -1763,6 +2721,32 @@ class ModuleGenerator {
1763
2721
  ${indent(item, 2)}`).join(",")}
1764
2722
  ]`;
1765
2723
  }
2724
+ renderCommands() {
2725
+ if (this.module.commands.length === 0)
2726
+ return "[]";
2727
+ return `[${this.module.commands.map((command) => {
2728
+ const fields = [
2729
+ `className: ${JSON.stringify(command.className)}`,
2730
+ `name: ${JSON.stringify(command.name)}`,
2731
+ `permission: ${JSON.stringify(command.permission ?? "")}`,
2732
+ `transaction: ${JSON.stringify(command.transaction)}`,
2733
+ ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2734
+ `idempotency: ${JSON.stringify(command.idempotency)}`,
2735
+ ...command.standalone ? ["standalone: true"] : [],
2736
+ ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2737
+ ];
2738
+ return `{ ${fields.join(", ")} }`;
2739
+ }).join(", ")}]`;
2740
+ }
2741
+ renderJobs() {
2742
+ const jobs = this.module.jobs ?? [];
2743
+ if (jobs.length === 0)
2744
+ return "[]";
2745
+ 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(", ")}]`;
2746
+ }
2747
+ renderAspects(aspects) {
2748
+ return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
2749
+ }
1766
2750
  renderServicesFactory() {
1767
2751
  return [
1768
2752
  `function create${this.pascal}Services(`,
@@ -1785,6 +2769,30 @@ ${indent(item, 2)}`).join(",")}
1785
2769
  indent(this.renderFactoryBody(kind), 2),
1786
2770
  `}`
1787
2771
  ].join(`
2772
+ `);
2773
+ }
2774
+ renderScopeDestroyer(kind) {
2775
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
2776
+ const plan = [];
2777
+ const multiIndices = new Map;
2778
+ for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
2779
+ const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
2780
+ if (index !== undefined)
2781
+ multiIndices.set(provider.token, index + 1);
2782
+ if (provider.kind !== "existing" && provider.hasOnDestroy) {
2783
+ plan.push({ key: camelName(provider.token), index });
2784
+ }
2785
+ }
2786
+ for (const controller of this.module.controllers) {
2787
+ if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
2788
+ plan.push({ key: camelName(controller.className) });
2789
+ }
2790
+ }
2791
+ return [
2792
+ `async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
2793
+ ` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
2794
+ `}`
2795
+ ].join(`
1788
2796
  `);
1789
2797
  }
1790
2798
  renderFactoryBody(kind) {
@@ -1825,39 +2833,76 @@ ${indent(item, 2)}`).join(",")}
1825
2833
  const key = camelName(provider.token);
1826
2834
  switch (provider.kind) {
1827
2835
  case "class": {
1828
- const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
1829
- const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
2836
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
2837
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1830
2838
  const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
1831
- return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
2839
+ return {
2840
+ constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
2841
+ key,
2842
+ expr: local
2843
+ };
1832
2844
  }
1833
2845
  case "value": {
1834
- const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
2846
+ const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
1835
2847
  const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1836
2848
  return { constLine: `const ${local} = ${expr};`, key, expr: local };
1837
2849
  }
1838
2850
  case "factory": {
1839
2851
  if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
1840
- const tokenIdent = this.imports.add(provider.token, provider.importPath);
2852
+ const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
1841
2853
  const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1842
- const constLine = `const ${local2} = typeof ${tokenIdent} === "object" && ${tokenIdent} && "factory" in ${tokenIdent} && typeof (${tokenIdent} as any).factory === "function" ? (${tokenIdent} as any).factory() : undefined;`;
2854
+ const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
1843
2855
  return { constLine, key, expr: local2 };
1844
2856
  }
1845
- const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
1846
- const args = provider.deps.map((dep) => this.depExpr(dep, kind, provider.optionalDeps?.includes(dep))).join(", ");
2857
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2858
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1847
2859
  const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
1848
2860
  return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
1849
2861
  }
1850
2862
  case "existing": {
1851
- return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
2863
+ return {
2864
+ key,
2865
+ expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
2866
+ };
1852
2867
  }
1853
2868
  }
1854
2869
  }
1855
2870
  emitController(controller, kind) {
1856
2871
  const className = this.imports.add(controller.className, controller.importPath);
1857
- const args = controller.deps.map((dep) => this.depExpr(dep, kind, controller.optionalDeps?.includes(dep))).join(", ");
2872
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
1858
2873
  const key = camelName(controller.className);
1859
2874
  const local = this.localVar(controller.className, kind);
1860
- return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
2875
+ return {
2876
+ constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
2877
+ key,
2878
+ expr: local
2879
+ };
2880
+ }
2881
+ instantiate(className, args, kind, functionalInjects) {
2882
+ if (!functionalInjects || functionalInjects.length === 0) {
2883
+ return `new ${className}(${args})`;
2884
+ }
2885
+ const clauses = functionalInjects.map((entry) => {
2886
+ const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
2887
+ const value = this.depExpr(entry.token, kind, {
2888
+ optional: entry.optional,
2889
+ self: entry.self,
2890
+ skipSelf: entry.skipSelf,
2891
+ host: entry.host
2892
+ });
2893
+ return `if (token === ${token}) return ${value} as T;`;
2894
+ });
2895
+ const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
2896
+ const injector = [
2897
+ `{`,
2898
+ `get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
2899
+ ...clauses,
2900
+ missing,
2901
+ `},`,
2902
+ `}`
2903
+ ].join(`
2904
+ `);
2905
+ return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
1861
2906
  }
1862
2907
  localVar(token, kind) {
1863
2908
  const locals = this.locals[kind];
@@ -1874,23 +2919,34 @@ ${indent(item, 2)}`).join(",")}
1874
2919
  locals.set(token, local);
1875
2920
  return local;
1876
2921
  }
1877
- depExpr(token, kind, isOptional = false) {
2922
+ depOptions(node, token) {
2923
+ return {
2924
+ optional: node.optionalDeps?.includes(token) ?? false,
2925
+ self: node.selfDeps?.includes(token) ?? false,
2926
+ skipSelf: node.skipSelfDeps?.includes(token) ?? false,
2927
+ host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
2928
+ };
2929
+ }
2930
+ depExpr(token, kind, options = {}) {
2931
+ const isOptional = options.optional ?? false;
2932
+ const isSelf = options.self ?? false;
2933
+ const isSkipSelf = options.skipSelf ?? false;
1878
2934
  if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
1879
2935
  return "ctx";
1880
2936
  if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
1881
2937
  return "ctx";
1882
2938
  const own = this.module.providers.find((p) => p.token === token);
1883
- if (own) {
2939
+ const ownIsLocal = own && factoryOfScope(own.scope) === kind;
2940
+ if (own && ownIsLocal && !isSkipSelf) {
1884
2941
  if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
1885
2942
  return this.locals[kind].get(token) ?? camelName(token);
1886
2943
  }
1887
- if (own.kind === "existing" && factoryOfScope(own.scope) === kind) {
1888
- return this.depExpr(own.useExisting ?? token, kind, isOptional);
1889
- }
1890
- if (kind === "services") {
1891
- return `services.${camelName(token)}`;
2944
+ if (own.kind === "existing") {
2945
+ return this.depExpr(own.useExisting ?? token, kind, options);
1892
2946
  }
1893
- return `services.${camelName(token)}`;
2947
+ }
2948
+ if (isSelf) {
2949
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1894
2950
  }
1895
2951
  for (const importName of this.module.imports) {
1896
2952
  const imported = this.graph.modules.find((m) => m.name === importName);
@@ -1909,6 +2965,8 @@ ${indent(item, 2)}`).join(",")}
1909
2965
  if (isOptional && !this.graph.externalTokens.includes(token)) {
1910
2966
  return "undefined";
1911
2967
  }
2968
+ if (isSelf)
2969
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1912
2970
  if (kind === "services")
1913
2971
  return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
1914
2972
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
@@ -2366,12 +3424,16 @@ var COMPILER_DIAGNOSTIC_CODES = {
2366
3424
  "conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
2367
3425
  "missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
2368
3426
  "missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
3427
+ "provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
3428
+ "unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
2369
3429
  "command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
2370
3430
  "duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
2371
3431
  "route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
2372
3432
  "command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
2373
3433
  "route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
2374
3434
  "command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
3435
+ "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3436
+ "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
2375
3437
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
2376
3438
  };
2377
3439
  function validateGraph(graph, options = false) {
@@ -2405,10 +3467,14 @@ function validateGraph(graph, options = false) {
2405
3467
  }
2406
3468
  }
2407
3469
  }
2408
- function resolveDep(module, token) {
2409
- const own = module.providers.find((p) => p.token === token);
2410
- if (own)
2411
- return { module, provider: own };
3470
+ function resolveDep(module, token, flags = {}) {
3471
+ if (!flags.skipSelf) {
3472
+ const own = module.providers.find((p) => p.token === token);
3473
+ if (own)
3474
+ return { module, provider: own };
3475
+ }
3476
+ if (flags.self)
3477
+ return;
2412
3478
  for (const importName of module.imports) {
2413
3479
  const imported = graph.modules.find((m) => m.name === importName);
2414
3480
  if (!imported || !imported.exports.includes(token))
@@ -2606,7 +3672,7 @@ function validateGraph(graph, options = false) {
2606
3672
  }
2607
3673
  if (controller.selfDeps && controller.selfDeps.length > 0) {
2608
3674
  for (const dep of controller.selfDeps) {
2609
- const own = module.providers.find((p) => p.token === dep);
3675
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2610
3676
  if (!own) {
2611
3677
  error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2612
3678
  }
@@ -2614,7 +3680,7 @@ function validateGraph(graph, options = false) {
2614
3680
  }
2615
3681
  if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
2616
3682
  for (const dep of controller.skipSelfDeps) {
2617
- const own = module.providers.find((p) => p.token === dep);
3683
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2618
3684
  if (own) {
2619
3685
  error("skip-self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, controller.file, undefined, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2620
3686
  }
@@ -2689,7 +3755,7 @@ function validateGraph(graph, options = false) {
2689
3755
  for (const provider of module.providers) {
2690
3756
  if (provider.selfDeps && provider.selfDeps.length > 0) {
2691
3757
  for (const dep of provider.selfDeps) {
2692
- const own = module.providers.find((p) => p.token === dep);
3758
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2693
3759
  if (!own) {
2694
3760
  error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2695
3761
  }
@@ -2697,7 +3763,7 @@ function validateGraph(graph, options = false) {
2697
3763
  }
2698
3764
  if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
2699
3765
  for (const dep of provider.skipSelfDeps) {
2700
- const own = module.providers.find((p) => p.token === dep);
3766
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2701
3767
  if (own) {
2702
3768
  error("skip-self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, provider.file, provider.line, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
2703
3769
  }
@@ -2705,7 +3771,10 @@ function validateGraph(graph, options = false) {
2705
3771
  }
2706
3772
  for (const dep of provider.deps) {
2707
3773
  const isOptional = provider.optionalDeps?.includes(dep);
2708
- const resolved = resolveDep(module, dep);
3774
+ const resolved = resolveDep(module, dep, {
3775
+ self: provider.selfDeps?.includes(dep),
3776
+ skipSelf: provider.skipSelfDeps?.includes(dep)
3777
+ });
2709
3778
  if (!resolved) {
2710
3779
  if (isOptional) {
2711
3780
  continue;
@@ -2713,6 +3782,8 @@ function validateGraph(graph, options = false) {
2713
3782
  if (!graph.externalTokens.includes(dep)) {
2714
3783
  if (globalProviders.has(dep)) {
2715
3784
  const owner = globalProviders.get(dep);
3785
+ if (!owner)
3786
+ continue;
2716
3787
  error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line, `Import module '${owner.module.name}' in '${module.name}', add '${dep}' to '${owner.module.name}' exports, or mark @Injectable({ providedIn: 'root' }).`);
2717
3788
  } else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
2718
3789
  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: () => ... }).`);
@@ -2773,7 +3844,8 @@ function validateGraph(graph, options = false) {
2773
3844
  }
2774
3845
  }
2775
3846
  if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
2776
- const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
3847
+ const allowedTags = rule.onlyDependOnLibsWithTags;
3848
+ const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
2777
3849
  if (!hasAllowed && targetTags.length > 0) {
2778
3850
  error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
2779
3851
  }
@@ -2795,6 +3867,8 @@ function validateGraph(graph, options = false) {
2795
3867
  referencedTokens.add(d);
2796
3868
  for (const d of ctrl.skipSelfDeps ?? [])
2797
3869
  referencedTokens.add(d);
3870
+ for (const d of ctrl.hostDeps ?? [])
3871
+ referencedTokens.add(d);
2798
3872
  }
2799
3873
  for (const p of mod.providers) {
2800
3874
  for (const d of p.deps ?? [])
@@ -2805,6 +3879,8 @@ function validateGraph(graph, options = false) {
2805
3879
  referencedTokens.add(d);
2806
3880
  for (const d of p.skipSelfDeps ?? [])
2807
3881
  referencedTokens.add(d);
3882
+ for (const d of p.hostDeps ?? [])
3883
+ referencedTokens.add(d);
2808
3884
  if (p.useExisting)
2809
3885
  referencedTokens.add(p.useExisting);
2810
3886
  }
@@ -2927,7 +4003,10 @@ function detectCycles(graph, resolveDep) {
2927
4003
  state.set(id, "visiting");
2928
4004
  stack.push(ref);
2929
4005
  for (const dep of ref.provider.deps) {
2930
- const resolved = resolveDep(ref.module, dep);
4006
+ const resolved = resolveDep(ref.module, dep, {
4007
+ self: ref.provider.selfDeps?.includes(dep),
4008
+ skipSelf: ref.provider.skipSelfDeps?.includes(dep)
4009
+ });
2931
4010
  if (resolved)
2932
4011
  visit(resolved);
2933
4012
  }
@@ -3040,6 +4119,8 @@ function detectOrphanModules(graph) {
3040
4119
  }
3041
4120
  while (queue.length > 0) {
3042
4121
  const current = queue.shift();
4122
+ if (!current)
4123
+ continue;
3043
4124
  const mod = moduleMap.get(current);
3044
4125
  if (!mod)
3045
4126
  continue;
@@ -3068,10 +4149,225 @@ function detectOrphanModules(graph) {
3068
4149
  }
3069
4150
 
3070
4151
  // src/compile.ts
3071
- import { existsSync as existsSync2, readFileSync } from "node:fs";
3072
- import { join as join3 } from "node:path";
4152
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4153
+ import { join as join4 } from "node:path";
4154
+
4155
+ // src/type-safety.ts
4156
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4157
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4158
+ import * as ts4 from "@typescript/typescript6";
4159
+ var DEFAULT_EXCLUDES = [
4160
+ "**/*.test.ts",
4161
+ "**/*.spec.ts",
4162
+ "**/test/**",
4163
+ "**/tests/**",
4164
+ "**/__tests__/**",
4165
+ "**/fixtures/**",
4166
+ "**/generated/**",
4167
+ "**/dist/**",
4168
+ "**/*.d.ts"
4169
+ ];
4170
+ var DIAGNOSTIC_META = {
4171
+ "generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
4172
+ "source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
4173
+ "source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
4174
+ "source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
4175
+ "source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
4176
+ };
4177
+ function scanGeneratedArtifacts(artifacts, strict = true) {
4178
+ const diagnostics = [];
4179
+ for (const [file, content] of Object.entries(artifacts)) {
4180
+ if (content === undefined)
4181
+ continue;
4182
+ const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
4183
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4184
+ diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
4185
+ }
4186
+ }
4187
+ return diagnostics;
4188
+ }
4189
+ function scanProductionSource(options) {
4190
+ const rootDir = resolve2(options.rootDir);
4191
+ const configPath = join3(rootDir, "tsconfig.json");
4192
+ const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
4193
+ options: {
4194
+ strict: true,
4195
+ skipLibCheck: true,
4196
+ target: ts4.ScriptTarget.ES2022,
4197
+ module: ts4.ModuleKind.ESNext
4198
+ },
4199
+ errors: []
4200
+ };
4201
+ const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
4202
+ const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
4203
+ const compilerOptions = { ...projectConfig.options, noEmit: true };
4204
+ const host = ts4.createCompilerHost(compilerOptions);
4205
+ host.getCurrentDirectory = () => rootDir;
4206
+ const program = ts4.createProgram(rootNames, compilerOptions, host);
4207
+ const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
4208
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
4209
+ const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
4210
+ const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
4211
+ severity: "error",
4212
+ code: "source-config",
4213
+ message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
4214
+ `),
4215
+ file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
4216
+ line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
4217
+ errorCode: `TS${diagnostic.code}`
4218
+ }));
4219
+ const checker = program.getTypeChecker();
4220
+ for (const sourceFile of sourceFiles) {
4221
+ scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
4222
+ }
4223
+ return diagnostics;
4224
+ }
4225
+ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
4226
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4227
+ diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
4228
+ }
4229
+ for (const node of descendants(sourceFile)) {
4230
+ if (ts4.isAsExpression(node)) {
4231
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4232
+ continue;
4233
+ const assertedType = node.type.getText(sourceFile);
4234
+ if (assertedType === "const")
4235
+ continue;
4236
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4237
+ } else if (ts4.isTypeAssertionExpression(node)) {
4238
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4239
+ continue;
4240
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4241
+ } else if (ts4.isNonNullExpression(node)) {
4242
+ diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
4243
+ }
4244
+ }
4245
+ for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
4246
+ const initializer = declaration.initializer;
4247
+ if (!initializer || declaration.type)
4248
+ continue;
4249
+ const declarationType = checker.getTypeAtLocation(declaration.name);
4250
+ const initializerType = checker.getTypeAtLocation(initializer);
4251
+ for (const name of bindingNames(declaration.name)) {
4252
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4253
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
4254
+ }
4255
+ }
4256
+ if (isAnyType(declarationType))
4257
+ continue;
4258
+ if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
4259
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
4260
+ }
4261
+ 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))) {
4262
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
4263
+ }
4264
+ }
4265
+ for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
4266
+ if (parameter.type)
4267
+ continue;
4268
+ for (const name of bindingNames(parameter.name)) {
4269
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4270
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
4271
+ }
4272
+ }
4273
+ }
4274
+ }
4275
+ function readProjectConfig2(configPath) {
4276
+ const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
4277
+ if (config.error)
4278
+ return { options: {}, errors: [config.error] };
4279
+ const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
4280
+ return { options: parsed.options, errors: parsed.errors };
4281
+ }
4282
+ function isProductionSource(rootDir, sourceFile, excludes, outDir) {
4283
+ const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
4284
+ if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
4285
+ return false;
4286
+ if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
4287
+ return false;
4288
+ return !excludes.some((pattern) => globMatches(relativePath, pattern));
4289
+ }
4290
+ function isProductionSourcePath(rootDir, filePath, excludes) {
4291
+ const relativePath = normalizeRelative(rootDir, filePath);
4292
+ return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
4293
+ }
4294
+ function globMatches(value, pattern) {
4295
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
4296
+ return new RegExp(`^${escaped}$`).test(value);
4297
+ }
4298
+ function bindingNames(name) {
4299
+ if (ts4.isIdentifier(name))
4300
+ return [name];
4301
+ return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
4302
+ }
4303
+ function isLiteralExpression(node) {
4304
+ if (!node)
4305
+ return false;
4306
+ return [
4307
+ ts4.SyntaxKind.StringLiteral,
4308
+ ts4.SyntaxKind.NumericLiteral,
4309
+ ts4.SyntaxKind.TrueKeyword,
4310
+ ts4.SyntaxKind.FalseKeyword
4311
+ ].includes(node.kind);
4312
+ }
4313
+ function isLiteralSyntax(node) {
4314
+ return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
4315
+ }
4316
+ function isLiteralType(type) {
4317
+ return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
4318
+ }
4319
+ function isAnyType(type) {
4320
+ return (type.flags & ts4.TypeFlags.Any) !== 0;
4321
+ }
4322
+ function isLetDeclaration(declaration) {
4323
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4324
+ }
4325
+ function isConstDeclaration(declaration) {
4326
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
4327
+ }
4328
+ function descendants(root) {
4329
+ const result = [];
4330
+ const visit = (node) => {
4331
+ result.push(node);
4332
+ ts4.forEachChild(node, visit);
4333
+ };
4334
+ ts4.forEachChild(root, visit);
4335
+ return result;
4336
+ }
4337
+ function descendantsOfKind2(root, predicate) {
4338
+ const result = [];
4339
+ const visit = (node) => {
4340
+ if (predicate(node))
4341
+ result.push(node);
4342
+ ts4.forEachChild(node, visit);
4343
+ };
4344
+ ts4.forEachChild(root, visit);
4345
+ return result;
4346
+ }
4347
+ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
4348
+ const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
4349
+ const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
4350
+ const meta = DIAGNOSTIC_META[code];
4351
+ return {
4352
+ severity: strict ? "error" : "warn",
4353
+ code,
4354
+ message,
4355
+ file,
4356
+ line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
4357
+ errorCode: meta.errorCode,
4358
+ docsUrl: meta.docsUrl
4359
+ };
4360
+ }
4361
+ function normalizeRelative(rootDir, filePath) {
4362
+ return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4363
+ }
4364
+ function isAnyKeyword(node) {
4365
+ return node.kind === ts4.SyntaxKind.AnyKeyword;
4366
+ }
4367
+
4368
+ // src/compile.ts
3073
4369
  async function compileProject(options) {
3074
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4370
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3075
4371
  const diagnostics = [
3076
4372
  ...graph.diagnostics ?? [],
3077
4373
  ...validateGraph(graph, {
@@ -3090,14 +4386,40 @@ async function compileProject(options) {
3090
4386
  diagnostic.severity = "error";
3091
4387
  }
3092
4388
  }
3093
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
3094
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
4389
+ const typeSafety = resolveTypeSafety(options);
4390
+ const rendered = renderApplication(graph, {
3095
4391
  rootDir: options.rootDir,
3096
4392
  outDir: options.outDir,
3097
4393
  generateClient: options.generateClient,
3098
4394
  generatePermissions: options.generatePermissions,
3099
4395
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3100
- }) : [];
4396
+ });
4397
+ if (typeSafety.scanProductionSource) {
4398
+ diagnostics.push(...scanProductionSource({
4399
+ rootDir: options.rootDir,
4400
+ include: options.include,
4401
+ outDir: options.outDir,
4402
+ strict: options.strict,
4403
+ ...typeSafety
4404
+ }));
4405
+ }
4406
+ if (typeSafety.noAnyInGenerated) {
4407
+ diagnostics.push(...scanGeneratedArtifacts({
4408
+ "application.ts": rendered.applicationCode,
4409
+ "client.ts": rendered.clientCode,
4410
+ "permissions.ts": rendered.permissionsCode
4411
+ }, options.strict ?? false));
4412
+ }
4413
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
4414
+ const generatedOptions = {
4415
+ rootDir: options.rootDir,
4416
+ outDir: options.outDir,
4417
+ generateClient: options.generateClient,
4418
+ generatePermissions: options.generatePermissions,
4419
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4420
+ artifactHashes: options.cache?.generatedHashes
4421
+ };
4422
+ const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
3101
4423
  const stats = graph.cacheStats ? {
3102
4424
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
3103
4425
  changedFiles: [],
@@ -3108,7 +4430,7 @@ async function compileProject(options) {
3108
4430
  return { diagnostics, graph, written, stats };
3109
4431
  }
3110
4432
  async function checkProject(options) {
3111
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4433
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3112
4434
  const diagnostics = [
3113
4435
  ...graph.diagnostics ?? [],
3114
4436
  ...validateGraph(graph, {
@@ -3127,6 +4449,7 @@ async function checkProject(options) {
3127
4449
  diagnostic.severity = "error";
3128
4450
  }
3129
4451
  }
4452
+ const typeSafety = resolveTypeSafety(options);
3130
4453
  const rendered = renderApplication(graph, {
3131
4454
  rootDir: options.rootDir,
3132
4455
  outDir: options.outDir,
@@ -3134,6 +4457,22 @@ async function checkProject(options) {
3134
4457
  generatePermissions: options.generatePermissions,
3135
4458
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3136
4459
  });
4460
+ if (typeSafety.scanProductionSource) {
4461
+ diagnostics.push(...scanProductionSource({
4462
+ rootDir: options.rootDir,
4463
+ include: options.include,
4464
+ outDir: options.outDir,
4465
+ strict: options.strict,
4466
+ ...typeSafety
4467
+ }));
4468
+ }
4469
+ if (typeSafety.noAnyInGenerated) {
4470
+ diagnostics.push(...scanGeneratedArtifacts({
4471
+ "application.ts": rendered.applicationCode,
4472
+ "client.ts": rendered.clientCode,
4473
+ "permissions.ts": rendered.permissionsCode
4474
+ }, options.strict ?? false));
4475
+ }
3137
4476
  const expectedFiles = {
3138
4477
  "application.ts": rendered.applicationCode,
3139
4478
  "app.manifest.json": rendered.manifestJson
@@ -3146,12 +4485,12 @@ async function checkProject(options) {
3146
4485
  }
3147
4486
  const mismatches = [];
3148
4487
  for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
3149
- const diskPath = join3(options.outDir, filename);
3150
- if (!existsSync2(diskPath)) {
4488
+ const diskPath = join4(options.outDir, filename);
4489
+ if (!existsSync3(diskPath)) {
3151
4490
  mismatches.push(`${filename}: generated artifact is missing from disk`);
3152
4491
  continue;
3153
4492
  }
3154
- const diskContent = readFileSync(diskPath, "utf8");
4493
+ const diskContent = readFileSync3(diskPath, "utf8");
3155
4494
  if (diskContent !== expectedContent) {
3156
4495
  mismatches.push(`${filename}: disk artifact differs from current compiler output`);
3157
4496
  }
@@ -3163,10 +4502,17 @@ async function checkProject(options) {
3163
4502
  graph
3164
4503
  };
3165
4504
  }
4505
+ function resolveTypeSafety(options) {
4506
+ return {
4507
+ noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
4508
+ scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
4509
+ exclude: options.typeSafety?.exclude
4510
+ };
4511
+ }
3166
4512
 
3167
4513
  // src/inspect.ts
3168
- import { existsSync as existsSync3 } from "node:fs";
3169
- import { join as join4 } from "node:path";
4514
+ import { existsSync as existsSync4 } from "node:fs";
4515
+ import { join as join5 } from "node:path";
3170
4516
  function formatGraph(graph) {
3171
4517
  const lines = [];
3172
4518
  for (const module of graph.modules) {
@@ -3207,13 +4553,13 @@ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
3207
4553
  const checks = [
3208
4554
  {
3209
4555
  name: "project-root",
3210
- ok: existsSync3(rootDir),
3211
- detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
4556
+ ok: existsSync4(rootDir),
4557
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
3212
4558
  },
3213
4559
  {
3214
4560
  name: "tsconfig",
3215
- ok: existsSync3(join4(rootDir, "tsconfig.json")),
3216
- detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
4561
+ ok: existsSync4(join5(rootDir, "tsconfig.json")),
4562
+ detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
3217
4563
  },
3218
4564
  {
3219
4565
  name: "modules",
@@ -3303,27 +4649,31 @@ function exportGraphDot(graph) {
3303
4649
 
3304
4650
  // src/watch.ts
3305
4651
  import { watch } from "node:fs";
3306
- import { relative as relative3, resolve as resolve2 } from "node:path";
4652
+ import { relative as relative4, resolve as resolve4 } from "node:path";
3307
4653
 
3308
4654
  // src/incremental.ts
3309
- import { createHash as createHash2 } from "node:crypto";
3310
- import { access, readdir, readFile } from "node:fs/promises";
3311
- import { relative as relative2, resolve, sep as sep2 } from "node:path";
4655
+ import { createHash as createHash5 } from "node:crypto";
4656
+ import { access as access2, readdir, readFile } from "node:fs/promises";
4657
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
3312
4658
  function createDependencyGraphCache() {
3313
4659
  return {
3314
4660
  modules: new Map,
3315
- fileHashes: new Map
4661
+ fileHashes: new Map,
4662
+ generatedHashes: new Map
3316
4663
  };
3317
4664
  }
3318
4665
  function createIncrementalCompiler() {
3319
4666
  let previousSnapshot;
3320
4667
  let previousResult;
4668
+ let previousCache;
3321
4669
  const cache = createDependencyGraphCache();
3322
4670
  return {
3323
4671
  async compile(options, changedPaths) {
3324
- const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
4672
+ const optionsKey = optionsKeyOf(options);
4673
+ const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
3325
4674
  const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
3326
- const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && changedFiles.length === 0);
4675
+ const activeCache = options.cache ?? cache;
4676
+ const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
3327
4677
  if (cacheHit && previousResult) {
3328
4678
  return {
3329
4679
  ...previousResult,
@@ -3336,22 +4686,14 @@ function createIncrementalCompiler() {
3336
4686
  }
3337
4687
  };
3338
4688
  }
3339
- if (previousResult && previousSnapshot && changedFiles.length > 0 && !await requiresGraphRebuild(options.rootDir, changedFiles)) {
3340
- const stats2 = {
3341
- cacheHit: true,
3342
- changedFiles,
3343
- affectedModules: findAffectedModules(previousResult.graph.modules, previousResult.graph.modules, changedFiles),
3344
- reusedModules: previousResult.graph.modules.map((m) => m.name),
3345
- reanalyzedModules: []
3346
- };
3347
- previousSnapshot = snapshot;
3348
- return { ...previousResult, written: [], stats: stats2 };
3349
- }
3350
- const activeCache = options.cache ?? cache;
3351
4689
  if (!activeCache.dependencyGraph && previousResult) {
3352
4690
  activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
3353
4691
  }
3354
- const result = await compileProject({ ...options, cache: activeCache });
4692
+ const result = await compileProject({
4693
+ ...options,
4694
+ cache: activeCache,
4695
+ changedPaths: changedFiles
4696
+ });
3355
4697
  const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
3356
4698
  const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
3357
4699
  const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
@@ -3367,14 +4709,18 @@ function createIncrementalCompiler() {
3367
4709
  };
3368
4710
  previousSnapshot = snapshot;
3369
4711
  previousResult = result;
4712
+ previousCache = activeCache;
3370
4713
  return { ...result, stats };
3371
4714
  },
3372
4715
  reset() {
3373
4716
  previousSnapshot = undefined;
3374
4717
  previousResult = undefined;
4718
+ previousCache = undefined;
3375
4719
  cache.modules.clear();
3376
4720
  cache.fileHashes.clear();
4721
+ cache.generatedHashes?.clear();
3377
4722
  cache.dependencyGraph = undefined;
4723
+ cache.programSession?.reset();
3378
4724
  },
3379
4725
  getCache() {
3380
4726
  return cache;
@@ -3382,18 +4728,21 @@ function createIncrementalCompiler() {
3382
4728
  };
3383
4729
  }
3384
4730
  async function updateSnapshot(previous, options, changedPaths) {
3385
- const rootDir = resolve(options.rootDir);
3386
- const outDir = resolve(options.outDir);
4731
+ const rootDir = resolve3(options.rootDir);
4732
+ const outDir = resolve3(options.outDir);
3387
4733
  const files = { ...previous.files };
3388
4734
  for (const changedPath of changedPaths) {
3389
- const absolutePath = resolve(rootDir, changedPath);
4735
+ const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4736
+ const relativeChangedPath = relative3(rootDir, absolutePath);
4737
+ if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
4738
+ continue;
3390
4739
  if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
3391
4740
  continue;
3392
- const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
4741
+ const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
3393
4742
  try {
3394
- await access(absolutePath);
4743
+ await access2(absolutePath);
3395
4744
  const content = await readFile(absolutePath);
3396
- files[relativePath] = createHash2("sha256").update(content).digest("hex");
4745
+ files[relativePath] = createHash5("sha256").update(content).digest("hex");
3397
4746
  } catch {
3398
4747
  delete files[relativePath];
3399
4748
  }
@@ -3401,20 +4750,23 @@ async function updateSnapshot(previous, options, changedPaths) {
3401
4750
  return { files, optionsKey: optionsKeyOf(options) };
3402
4751
  }
3403
4752
  async function createSnapshot(options) {
3404
- const rootDir = resolve(options.rootDir);
3405
- const outDir = resolve(options.outDir);
4753
+ const rootDir = resolve3(options.rootDir);
4754
+ const outDir = resolve3(options.outDir);
3406
4755
  const paths = await listSourceFiles(rootDir, outDir);
3407
4756
  const files = {};
3408
4757
  for (const path of paths) {
3409
4758
  const content = await readFile(path);
3410
- files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
4759
+ files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
3411
4760
  }
3412
4761
  return { files, optionsKey: optionsKeyOf(options) };
3413
4762
  }
3414
4763
  function optionsKeyOf(options) {
3415
4764
  return JSON.stringify({
4765
+ rootDir: resolve3(options.rootDir),
4766
+ outDir: resolve3(options.outDir),
3416
4767
  include: options.include,
3417
4768
  strict: options.strict,
4769
+ writeOnError: options.writeOnError,
3418
4770
  moduleBoundaryPreset: options.moduleBoundaryPreset,
3419
4771
  moduleBoundaries: options.moduleBoundaries,
3420
4772
  allowRouteCommandBindings: options.allowRouteCommandBindings,
@@ -3422,27 +4774,16 @@ function optionsKeyOf(options) {
3422
4774
  disallowControllerDirectDb: options.disallowControllerDirectDb,
3423
4775
  detectOrphanModules: options.detectOrphanModules,
3424
4776
  generateClient: options.generateClient,
3425
- generatePermissions: options.generatePermissions
4777
+ generatePermissions: options.generatePermissions,
4778
+ typeSafety: options.typeSafety,
4779
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
3426
4780
  });
3427
4781
  }
3428
- async function requiresGraphRebuild(rootDir, changedFiles) {
3429
- for (const relativePath of changedFiles) {
3430
- const path = resolve(rootDir, relativePath);
3431
- try {
3432
- const source = await readFile(path, "utf8");
3433
- if (/@(?:Module|Injectable|Inject|Controller|Command|Query)\b|new\s+InjectionToken\b|\bdefineModule\s*\(/.test(source))
3434
- return true;
3435
- } catch {
3436
- return true;
3437
- }
3438
- }
3439
- return false;
3440
- }
3441
4782
  async function listSourceFiles(rootDir, outDir) {
3442
4783
  const result = [];
3443
4784
  const visit = async (directory) => {
3444
4785
  for (const entry of await readdir(directory, { withFileTypes: true })) {
3445
- const path = resolve(directory, entry.name);
4786
+ const path = resolve3(directory, entry.name);
3446
4787
  if (entry.isDirectory()) {
3447
4788
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
3448
4789
  continue;
@@ -3500,7 +4841,9 @@ class ModuleDependencyGraph {
3500
4841
  if (!this.dependents.has(imp)) {
3501
4842
  this.dependents.set(imp, new Set);
3502
4843
  }
3503
- this.dependents.get(imp).add(modName);
4844
+ const dependents = this.dependents.get(imp);
4845
+ if (dependents)
4846
+ dependents.add(modName);
3504
4847
  }
3505
4848
  }
3506
4849
  }
@@ -3511,7 +4854,9 @@ class ModuleDependencyGraph {
3511
4854
  if (!this.fileOwners.has(normalized)) {
3512
4855
  this.fileOwners.set(normalized, new Set);
3513
4856
  }
3514
- this.fileOwners.get(normalized).add(moduleName);
4857
+ const owners = this.fileOwners.get(normalized);
4858
+ if (owners)
4859
+ owners.add(moduleName);
3515
4860
  }
3516
4861
  getModulesOwningFile(filePath) {
3517
4862
  const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
@@ -3527,12 +4872,14 @@ class ModuleDependencyGraph {
3527
4872
  }
3528
4873
  }
3529
4874
  if (directlyAffected.size === 0) {
3530
- return Array.from(this.moduleMap.keys());
4875
+ return [];
3531
4876
  }
3532
4877
  const affected = new Set(directlyAffected);
3533
4878
  const queue = Array.from(directlyAffected);
3534
4879
  while (queue.length > 0) {
3535
4880
  const current = queue.shift();
4881
+ if (!current)
4882
+ continue;
3536
4883
  const dependents = this.dependents.get(current);
3537
4884
  if (dependents) {
3538
4885
  for (const dep of dependents) {
@@ -3560,8 +4907,8 @@ function findAffectedModules(previous, current, changedFiles) {
3560
4907
  // src/watch.ts
3561
4908
  var DEFAULT_DEBOUNCE_MS = 100;
3562
4909
  function watchProject(options) {
3563
- const rootDir = resolve2(options.rootDir);
3564
- const outDir = resolve2(options.outDir);
4910
+ const rootDir = resolve4(options.rootDir);
4911
+ const outDir = resolve4(options.outDir);
3565
4912
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3566
4913
  let timer;
3567
4914
  let closed = false;
@@ -3571,8 +4918,12 @@ function watchProject(options) {
3571
4918
  let watcher;
3572
4919
  const incremental = createIncrementalCompiler();
3573
4920
  let initialEvent;
3574
- let resolveReady;
3575
- let rejectReady;
4921
+ let resolveReady = () => {
4922
+ return;
4923
+ };
4924
+ let rejectReady = () => {
4925
+ return;
4926
+ };
3576
4927
  const ready = new Promise((resolvePromise, rejectPromise) => {
3577
4928
  resolveReady = resolvePromise;
3578
4929
  rejectReady = rejectPromise;
@@ -3641,12 +4992,12 @@ function watchProject(options) {
3641
4992
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
3642
4993
  if (!filename)
3643
4994
  return schedule();
3644
- const changedPath = resolve2(rootDir, filename.toString());
3645
- const relativePath = relative3(outDir, changedPath);
4995
+ const changedPath = resolve4(rootDir, filename.toString());
4996
+ const relativePath = relative4(outDir, changedPath);
3646
4997
  if (!relativePath.startsWith("..") && relativePath !== "")
3647
4998
  return;
3648
4999
  if (/\.(tsx?|mts|cts)$/.test(changedPath))
3649
- schedule(relative3(rootDir, changedPath));
5000
+ schedule(relative4(rootDir, changedPath));
3650
5001
  });
3651
5002
  if (initialEvent)
3652
5003
  resolveReady(initialEvent);
@@ -3668,6 +5019,9 @@ function watchProject(options) {
3668
5019
  }
3669
5020
 
3670
5021
  // src/cli.ts
5022
+ function isModuleBoundaryPresetName(value) {
5023
+ return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
5024
+ }
3671
5025
  function printUsage() {
3672
5026
  console.log(`
3673
5027
  @supacloud/compiler CLI
@@ -3691,7 +5045,7 @@ Commands:
3691
5045
  Options:
3692
5046
  --root, -r <dir> Application source root (default: current directory or first positional argument)
3693
5047
  --out, -o <dir> Artifact output directory (default: <rootDir>/generated)
3694
- --strict Treat all warnings as errors
5048
+ --strict Enable type-safety gates and treat all warnings as errors
3695
5049
  --client Generate typed API client in client.ts
3696
5050
  --permissions Generate typed permissions registry in permissions.ts
3697
5051
  --debounce <ms> Debounce source changes in dev mode (default: 100)
@@ -3742,7 +5096,12 @@ async function run() {
3742
5096
  } else if (arg === "--json") {
3743
5097
  json = true;
3744
5098
  } else if (arg === "--preset" || arg === "-p") {
3745
- preset = args[++i];
5099
+ const presetArg = args[++i];
5100
+ if (!isModuleBoundaryPresetName(presetArg)) {
5101
+ console.error(`Error: --preset requires a known preset, received "${presetArg ?? ""}"`);
5102
+ process.exit(1);
5103
+ }
5104
+ preset = presetArg;
3746
5105
  } else if (!arg.startsWith("-") && rootDir === ".") {
3747
5106
  if (command === "explain" && !query)
3748
5107
  query = arg;
@@ -3752,8 +5111,8 @@ async function run() {
3752
5111
  query = arg;
3753
5112
  }
3754
5113
  }
3755
- const resolvedRoot = resolve3(process.cwd(), rootDir);
3756
- const resolvedOut = outDir ? resolve3(process.cwd(), outDir) : resolve3(resolvedRoot, "generated");
5114
+ const resolvedRoot = resolve5(process.cwd(), rootDir);
5115
+ const resolvedOut = outDir ? resolve5(process.cwd(), outDir) : resolve5(resolvedRoot, "generated");
3757
5116
  if (command === "compile") {
3758
5117
  const result = await compileProject({
3759
5118
  rootDir: resolvedRoot,