@supacloud/compiler 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,17 +1,388 @@
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/util.ts
333
+ function camelName(token) {
334
+ const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
335
+ if (isConstantCase) {
336
+ return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
337
+ }
338
+ return token.charAt(0).toLowerCase() + token.slice(1);
339
+ }
340
+ function relativeImportPath(fromDir, toFile) {
341
+ const fromParts = fromDir.split("/").filter(Boolean);
342
+ const toParts = toFile.split("/").filter(Boolean);
343
+ let common = 0;
344
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
345
+ common += 1;
346
+ }
347
+ const ups = fromParts.length - common;
348
+ const downs = toParts.slice(common);
349
+ const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
350
+ const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
351
+ const joined = segments.join("/");
352
+ return joined.startsWith("..") ? joined : `./${joined}`;
353
+ }
354
+ var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
355
+ var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
356
+ function isRequestContextToken(token, tokenNames) {
357
+ return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
358
+ }
359
+ function isJobContextToken(token, tokenNames) {
360
+ return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
361
+ }
362
+ function joinRoutePaths(prefix, path) {
363
+ const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
364
+ const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
365
+ return normalized;
366
+ }
367
+ function findClosestMatch(target, candidates) {
368
+ if (candidates.length === 0)
369
+ return;
370
+ if (candidates.length === 1)
371
+ return candidates[0];
372
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
373
+ const targetNorm = norm(target);
374
+ for (const c of candidates) {
375
+ if (norm(c) === targetNorm)
376
+ return c;
377
+ }
378
+ for (const c of candidates) {
379
+ if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
380
+ return c;
381
+ }
382
+ return candidates[0];
383
+ }
384
+
385
+ // src/analyze.ts
15
386
  var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
16
387
  var ROUTE_DECORATORS = {
17
388
  Get: "GET",
@@ -23,63 +394,118 @@ var ROUTE_DECORATORS = {
23
394
  Options: "OPTIONS"
24
395
  };
25
396
  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()));
397
+ function isScope(value) {
398
+ return SCOPES.some((scope) => scope === value);
399
+ }
400
+ function nodeText(node) {
401
+ return node.getText(node.getSourceFile());
402
+ }
403
+ function lineOf(node) {
404
+ const sourceFile = node.getSourceFile();
405
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
406
+ }
407
+ function variableName(decl) {
408
+ return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
409
+ }
410
+ function propertyName(name) {
411
+ if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
412
+ return name.text;
413
+ if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
414
+ return name.text;
415
+ return nodeText(name);
416
+ }
417
+ function parameterName(param) {
418
+ return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
419
+ }
420
+ function decoratorsOf(node) {
421
+ return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
422
+ }
423
+ function decoratorArguments(dec) {
424
+ return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
425
+ }
426
+ function hasMethod(cls, name) {
427
+ return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
428
+ }
429
+ function hasDestroyHook(cls) {
430
+ return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
431
+ }
432
+ function descendantsOfKind(root, predicate) {
433
+ const result = [];
434
+ const visit = (node) => {
435
+ if (predicate(node))
436
+ result.push(node);
437
+ ts3.forEachChild(node, visit);
438
+ };
439
+ visit(root);
440
+ return result;
441
+ }
442
+ async function analyzeProject(rootDir, include, cache, changedPaths) {
443
+ const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
444
+ if (cache)
445
+ cache.programSession = session;
446
+ const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
447
+ const update = session.update(rootNames, changedPaths);
448
+ const program = update.program;
449
+ const checker = program.getTypeChecker();
450
+ 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
451
  const ctx = {
42
452
  rootDir,
453
+ program,
454
+ checker,
43
455
  tokensByName: new Map,
44
456
  classesByName: new Map,
45
457
  diagnostics: []
46
458
  };
459
+ const nativeTraitFiles = new Map;
460
+ for (const diagnostic of session.getDiagnostics()) {
461
+ ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
462
+ }
463
+ for (const trait of session.getTraits()) {
464
+ const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
465
+ kinds.add(trait.kind);
466
+ nativeTraitFiles.set(trait.file, kinds);
467
+ }
47
468
  for (const sf of sourceFiles) {
48
469
  indexFile(sf, ctx);
49
470
  }
50
471
  const candidates = [];
51
472
  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
- });
473
+ const traits = nativeTraitFiles.get(sf.fileName);
474
+ if (!cache || traits?.has("module")) {
475
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
476
+ const moduleDec = findDecorator(cls, "Module");
477
+ if (!moduleDec)
478
+ continue;
479
+ const options = decoratorObjectArg(moduleDec);
480
+ if (!options)
481
+ continue;
482
+ candidates.push({
483
+ node: cls,
484
+ options,
485
+ className: cls.name?.text ?? "<anonymous>",
486
+ file: sf.fileName,
487
+ line: lineOf(cls)
488
+ });
489
+ }
66
490
  }
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
- });
491
+ if (!cache || traits?.has("defineModule")) {
492
+ for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
493
+ if (nodeText(call.expression) !== "defineModule")
494
+ continue;
495
+ const parent = call.parent;
496
+ if (!parent || !ts3.isVariableDeclaration(parent))
497
+ continue;
498
+ const arg = call.arguments[0];
499
+ if (!arg || !ts3.isObjectLiteralExpression(arg))
500
+ continue;
501
+ candidates.push({
502
+ node: parent,
503
+ options: arg,
504
+ className: variableName(parent),
505
+ file: sf.fileName,
506
+ line: lineOf(parent)
507
+ });
508
+ }
83
509
  }
84
510
  }
85
511
  const nameByNode = new Map;
@@ -92,8 +518,8 @@ async function analyzeProject(rootDir, include, cache) {
92
518
  if (cache) {
93
519
  const currentFileHashes = new Map;
94
520
  for (const sf of sourceFiles) {
95
- const rel = sourcePath(rootDir, sf.getFilePath());
96
- const hash = createHash("sha256").update(sf.getFullText()).digest("hex");
521
+ const rel = sourcePath(rootDir, sf.fileName);
522
+ const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
97
523
  currentFileHashes.set(rel, hash);
98
524
  }
99
525
  const changedFiles = new Set;
@@ -109,7 +535,7 @@ async function analyzeProject(rootDir, include, cache) {
109
535
  }
110
536
  const modulesToKeep = new Map;
111
537
  const finalModules = [];
112
- const finalDiagnostics = [];
538
+ const finalDiagnostics = [...ctx.diagnostics];
113
539
  const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
114
540
  for (const [modName, entry] of cache.modules.entries()) {
115
541
  const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
@@ -131,14 +557,7 @@ async function analyzeProject(rootDir, include, cache) {
131
557
  const diagBefore = ctx.diagnostics.length;
132
558
  const parsed = parseModule(c, nameByNode, ctx);
133
559
  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);
560
+ const ownedFiles = collectModuleSourceClosure(parsed, ctx);
142
561
  const fileHashes = {};
143
562
  for (const f of ownedFiles) {
144
563
  fileHashes[f] = currentFileHashes.get(f) ?? "";
@@ -165,6 +584,7 @@ async function analyzeProject(rootDir, include, cache) {
165
584
  } else {
166
585
  modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
167
586
  }
587
+ modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
168
588
  const allRegisteredClasses = new Set;
169
589
  const allRegisteredControllers = new Set;
170
590
  const allRegisteredCommands = new Set;
@@ -189,9 +609,9 @@ async function analyzeProject(rootDir, include, cache) {
189
609
  if (!allRegisteredClasses.has(name)) {
190
610
  const injectable = parseInjectableOptions(classInfo.decl, ctx);
191
611
  if (injectable?.providedIn === "root") {
192
- const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
612
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
193
613
  const file = sourcePath(ctx.rootDir, classInfo.file);
194
- const line = classInfo.decl.getStartLineNumber();
614
+ const line = lineOf(classInfo.decl);
195
615
  if (missing) {
196
616
  warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
197
617
  }
@@ -206,8 +626,9 @@ async function analyzeProject(rootDir, include, cache) {
206
626
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
207
627
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
208
628
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
629
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
209
630
  providedIn: "root",
210
- hasOnDestroy: classInfo.decl.getMethod("onDestroy") !== undefined || undefined,
631
+ hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
211
632
  exported: true,
212
633
  file,
213
634
  line,
@@ -218,8 +639,8 @@ async function analyzeProject(rootDir, include, cache) {
218
639
  if (!allRegisteredControllers.has(name)) {
219
640
  const controllerDec = findDecorator(classInfo.decl, "Controller");
220
641
  if (controllerDec) {
221
- const arg = controllerDec.getArguments()[0];
222
- const isStandalone = arg && Node.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
642
+ const arg = decoratorArguments(controllerDec)[0];
643
+ const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
223
644
  if (isStandalone) {
224
645
  const ctrl = parseController(classInfo.decl, ctx);
225
646
  if (ctrl)
@@ -233,13 +654,14 @@ async function analyzeProject(rootDir, include, cache) {
233
654
  const meta = decoratorObjectArg(commandDec);
234
655
  if (meta && booleanProp(meta, "standalone")) {
235
656
  standaloneCommands.push({
236
- className: classInfo.decl.getName() ?? name,
237
- name: stringLiteralProp(meta, "name") ?? classInfo.decl.getName() ?? name,
657
+ className: classInfo.decl.name?.text ?? name,
658
+ name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
238
659
  permission: stringLiteralProp(meta, "permission"),
239
660
  transaction: commandModeProp(meta, "transaction") ?? "none",
240
661
  audit: stringLiteralProp(meta, "audit"),
241
662
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
242
- standalone: true
663
+ standalone: true,
664
+ aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
243
665
  });
244
666
  }
245
667
  }
@@ -264,13 +686,13 @@ async function analyzeProject(rootDir, include, cache) {
264
686
  if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
265
687
  const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
266
688
  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
- }
689
+ modules = modules.map((module) => module === existingRoot ? {
690
+ ...module,
691
+ providers: [...module.providers, ...rootProviders],
692
+ controllers: [...module.controllers, ...standaloneControllers],
693
+ commands: [...module.commands, ...standaloneCommands],
694
+ exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
695
+ } : module);
274
696
  } else {
275
697
  const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
276
698
  modules.unshift({
@@ -309,25 +731,72 @@ async function analyzeProject(rootDir, include, cache) {
309
731
  cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
310
732
  };
311
733
  }
312
- function createProject(rootDir) {
313
- const tsConfigFilePath = join(rootDir, "tsconfig.json");
314
- if (existsSync(tsConfigFilePath)) {
315
- return new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true });
734
+ function collectModuleSourceClosure(module, ctx) {
735
+ const seeds = new Set;
736
+ const addRelativeModule = (path) => {
737
+ if (!path)
738
+ return;
739
+ const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
740
+ seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
741
+ };
742
+ addRelativeModule(module.file);
743
+ for (const provider of module.providers)
744
+ addRelativeModule(provider.importPath);
745
+ for (const controller of module.controllers)
746
+ addRelativeModule(controller.importPath);
747
+ const ownedFiles = new Set;
748
+ const queue = [...seeds];
749
+ while (queue.length > 0) {
750
+ const fileName = queue.shift();
751
+ if (!fileName)
752
+ continue;
753
+ const sourceFile = ctx.program.getSourceFile(fileName);
754
+ if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
755
+ continue;
756
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
757
+ if (ownedFiles.has(relativeFile))
758
+ continue;
759
+ ownedFiles.add(relativeFile);
760
+ for (const statement of sourceFile.statements) {
761
+ let moduleName;
762
+ if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
763
+ moduleName = statement.moduleSpecifier.text;
764
+ } else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
765
+ moduleName = statement.moduleSpecifier.text;
766
+ } else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
767
+ moduleName = statement.moduleReference.expression.text;
768
+ }
769
+ if (!moduleName || moduleName.startsWith("node:"))
770
+ continue;
771
+ const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
772
+ if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
773
+ queue.push(resolved);
774
+ }
316
775
  }
317
- return new Project({
318
- compilerOptions: { experimentalDecorators: true, allowJs: false }
319
- });
776
+ return ownedFiles;
777
+ }
778
+ function resolveSourcePath(rootDir, file) {
779
+ const normalized = file.replace(/\\/g, "/");
780
+ return resolvePath(rootDir, normalized);
781
+ }
782
+ function isProjectSourcePath(fileName, rootDir) {
783
+ const normalized = fileName.replace(/\\/g, "/");
784
+ const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
785
+ return normalized === root || normalized.startsWith(`${root}/`);
786
+ }
787
+ function isProjectSourceFile(sourceFile, rootDir) {
788
+ return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
320
789
  }
321
790
  function indexFile(sf, ctx) {
322
- for (const cls of sf.getClasses()) {
323
- const name = cls.getName();
791
+ for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
792
+ const name = cls.name?.text;
324
793
  if (name && !ctx.classesByName.has(name)) {
325
- ctx.classesByName.set(name, { name, decl: cls, file: sf.getFilePath() });
794
+ ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
326
795
  }
327
796
  }
328
- for (const statement of sf.getVariableStatements()) {
329
- for (const decl of statement.getDeclarations()) {
330
- const info = parseTokenVariable(decl, sf.getFilePath());
797
+ for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
798
+ for (const decl of statement.declarationList.declarations) {
799
+ const info = parseTokenVariable(decl, sf.fileName);
331
800
  if (info && !ctx.tokensByName.has(info.name)) {
332
801
  ctx.tokensByName.set(info.name, info);
333
802
  }
@@ -335,19 +804,19 @@ function indexFile(sf, ctx) {
335
804
  }
336
805
  }
337
806
  function parseTokenVariable(decl, file) {
338
- const init = decl.getInitializer();
339
- if (!init || !Node.isNewExpression(init))
807
+ const init = decl.initializer;
808
+ if (!init || !ts3.isNewExpression(init))
340
809
  return;
341
- if (init.getExpression().getText() !== "InjectionToken")
810
+ if (nodeText(init.expression) !== "InjectionToken")
342
811
  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();
812
+ const [nameArg, optionsArg] = init.arguments ?? [];
813
+ const info = { name: variableName(decl), file, line: lineOf(decl) };
814
+ if (nameArg && ts3.isStringLiteral(nameArg)) {
815
+ info.stringName = nameArg.text;
347
816
  }
348
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
817
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
349
818
  const scope = stringLiteralProp(optionsArg, "scope");
350
- if (scope && SCOPES.includes(scope)) {
819
+ if (scope && isScope(scope)) {
351
820
  info.scope = scope;
352
821
  }
353
822
  const providedIn = stringLiteralProp(optionsArg, "providedIn");
@@ -364,33 +833,76 @@ function parseTokenVariable(decl, file) {
364
833
  function parseModule(candidate, nameByNode, ctx) {
365
834
  const { options, className, file, line } = candidate;
366
835
  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);
836
+ const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
837
+ const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
368
838
  const imports = arrayProp(options, "imports").map((el) => {
369
839
  const unwrapped = unwrapForwardRef(el);
370
- const decl = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
840
+ const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
371
841
  if (decl) {
372
842
  const known = nameByNode.get(decl);
373
843
  if (known)
374
844
  return known;
375
- if (Node.isClassDeclaration(decl)) {
845
+ if (ts3.isClassDeclaration(decl)) {
376
846
  const dec = findDecorator(decl, "Module");
377
847
  const decOptions = dec && decoratorObjectArg(dec);
378
848
  const decName = decOptions && stringLiteralProp(decOptions, "name");
379
- return decName ?? decl.getName() ?? el.getText();
849
+ return decName ?? decl.name?.text ?? nodeText(el);
380
850
  }
381
- if (Node.isVariableDeclaration(decl))
382
- return decl.getName();
851
+ if (ts3.isVariableDeclaration(decl))
852
+ return variableName(decl);
383
853
  }
384
- return el.getText();
854
+ return nodeText(el);
385
855
  }).filter((v, i, arr) => arr.indexOf(v) === i);
386
856
  const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
387
857
  const exportsSet = new Set(exports);
388
858
  const providers = [];
389
- for (const el of arrayProp(options, "providers")) {
859
+ for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
860
+ const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
861
+ if (parsedProviders) {
862
+ providers.push(...parsedProviders);
863
+ continue;
864
+ }
865
+ if (ts3.isCallExpression(el)) {
866
+ const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
867
+ warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
868
+ continue;
869
+ }
390
870
  const provider = parseProvider(el, exportsSet, ctx);
391
871
  if (provider)
392
872
  providers.push(provider);
393
873
  }
874
+ for (const el of arrayProp(options, "jobs")) {
875
+ if (!ts3.isIdentifier(el))
876
+ continue;
877
+ const decl = resolveDeclaration(el, ctx)[0];
878
+ if (!decl || !ts3.isClassDeclaration(decl))
879
+ continue;
880
+ const className2 = decl.name?.text ?? el.text;
881
+ const registeredProvider = providers.find((provider) => provider.token === className2 || provider.useClass === className2);
882
+ if (registeredProvider)
883
+ continue;
884
+ const deps = classDeps(decl, ctx);
885
+ const injectable = parseInjectableOptions(decl, ctx);
886
+ const scope = injectable?.scope ?? "job";
887
+ providers.push({
888
+ token: className2,
889
+ tokenKind: "class",
890
+ kind: "class",
891
+ useClass: className2,
892
+ scope,
893
+ deps: deps.deps,
894
+ optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
895
+ selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
896
+ skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
897
+ hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
898
+ functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
899
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
900
+ exported: exportsSet.has(className2),
901
+ file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
902
+ line: lineOf(decl),
903
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
904
+ });
905
+ }
394
906
  const controllers = [];
395
907
  for (const el of arrayProp(options, "controllers")) {
396
908
  const controller = parseController(el, ctx);
@@ -400,40 +912,74 @@ function parseModule(candidate, nameByNode, ctx) {
400
912
  const handlerClasses = [];
401
913
  const seenHandlers = new Set;
402
914
  const collectHandler = (expr) => {
403
- if (!Node.isIdentifier(expr))
915
+ if (!ts3.isIdentifier(expr))
404
916
  return;
405
- const decl = resolveDeclaration(expr)[0];
406
- if (decl && Node.isClassDeclaration(decl) && !seenHandlers.has(decl.getName() ?? "")) {
407
- seenHandlers.add(decl.getName() ?? "");
917
+ const decl = resolveDeclaration(expr, ctx)[0];
918
+ if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
919
+ seenHandlers.add(decl.name?.text ?? "");
408
920
  handlerClasses.push(decl);
409
921
  }
410
922
  };
411
923
  for (const el of arrayProp(options, "providers")) {
412
- if (Node.isIdentifier(el))
924
+ if (ts3.isIdentifier(el))
413
925
  collectHandler(el);
414
- if (Node.isObjectLiteralExpression(el)) {
926
+ if (ts3.isObjectLiteralExpression(el)) {
415
927
  const useClass = getProp(el, "useClass");
416
928
  if (useClass)
417
929
  collectHandler(useClass);
418
930
  }
419
931
  }
420
932
  arrayProp(options, "commands").forEach(collectHandler);
933
+ arrayProp(options, "jobs").forEach(collectHandler);
421
934
  arrayProp(options, "queries").forEach(collectHandler);
422
935
  const commands = [];
936
+ const jobs = [];
423
937
  const queries = [];
424
938
  for (const cls of handlerClasses) {
425
939
  const commandDec = findDecorator(cls, "Command");
426
940
  if (commandDec) {
427
941
  const meta = decoratorObjectArg(commandDec);
428
942
  if (meta) {
943
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
429
944
  commands.push({
430
- className: cls.getName() ?? "<anonymous>",
431
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>",
945
+ className: cls.name?.text ?? "<anonymous>",
946
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
432
947
  permission: stringLiteralProp(meta, "permission"),
433
948
  transaction: commandModeProp(meta, "transaction") ?? "none",
434
949
  audit: stringLiteralProp(meta, "audit"),
435
950
  idempotency: commandModeProp(meta, "idempotency") ?? "none",
436
- standalone: booleanProp(meta, "standalone") || undefined
951
+ ...booleanProp(meta, "standalone") ? { standalone: true } : {},
952
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
953
+ });
954
+ }
955
+ }
956
+ const jobDec = findDecorator(cls, "Job");
957
+ if (jobDec) {
958
+ const meta = decoratorObjectArg(jobDec);
959
+ if (meta) {
960
+ const injectable = parseInjectableOptions(cls, ctx);
961
+ const className2 = cls.name?.text ?? "<anonymous>";
962
+ const provider = providers.find((candidate2) => candidate2.token === className2 || candidate2.useClass === className2);
963
+ const scope = provider?.scope ?? injectable?.scope ?? "job";
964
+ if (scope === "request") {
965
+ ctx.diagnostics.push({
966
+ severity: "error",
967
+ code: "invalid-job-scope",
968
+ message: `job ${className2} 不能使用 request scope;Job 只能使用 application 或 job scope`,
969
+ file: sourcePath(ctx.rootDir, cls.getSourceFile().fileName),
970
+ line: lineOf(cls),
971
+ suggestion: "移除 request scope,或改用 application/job scope。",
972
+ errorCode: "SC4007",
973
+ docsUrl: "https://supacloud.dev/errors/SC4007"
974
+ });
975
+ }
976
+ const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${className2}`);
977
+ jobs.push({
978
+ className: className2,
979
+ name: stringLiteralProp(meta, "name") ?? className2,
980
+ serviceKey: camelName(provider?.token ?? className2),
981
+ scope,
982
+ ...aspects2.length > 0 ? { aspects: aspects2 } : {}
437
983
  });
438
984
  }
439
985
  }
@@ -442,8 +988,8 @@ function parseModule(candidate, nameByNode, ctx) {
442
988
  const meta = decoratorObjectArg(queryDec);
443
989
  if (meta) {
444
990
  queries.push({
445
- className: cls.getName() ?? "<anonymous>",
446
- name: stringLiteralProp(meta, "name") ?? cls.getName() ?? "<anonymous>"
991
+ className: cls.name?.text ?? "<anonymous>",
992
+ name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
447
993
  });
448
994
  }
449
995
  }
@@ -458,7 +1004,9 @@ function parseModule(candidate, nameByNode, ctx) {
458
1004
  providers,
459
1005
  controllers,
460
1006
  commands,
1007
+ jobs,
461
1008
  queries,
1009
+ ...aspects.length > 0 ? { aspects } : {},
462
1010
  exports
463
1011
  };
464
1012
  }
@@ -467,14 +1015,14 @@ function commandModeProp(object, name) {
467
1015
  return value === "required" || value === "none" ? value : undefined;
468
1016
  }
469
1017
  function parseProvider(el, exportsSet, ctx) {
470
- const file = sourcePath(ctx.rootDir, el.getSourceFile().getFilePath());
471
- const line = el.getStartLineNumber();
1018
+ const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
1019
+ const line = lineOf(el);
472
1020
  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 };
1021
+ if (ts3.isIdentifier(unwrappedEl)) {
1022
+ const decl = resolveDeclaration(unwrappedEl, ctx)[0];
1023
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
1024
+ const className = cls?.name?.text ?? unwrappedEl.text;
1025
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
478
1026
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
479
1027
  if (missing) {
480
1028
  warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
@@ -490,15 +1038,16 @@ function parseProvider(el, exportsSet, ctx) {
490
1038
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
491
1039
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
492
1040
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1041
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
493
1042
  providedIn: injectable?.providedIn,
494
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1043
+ hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
495
1044
  exported: exportsSet.has(className),
496
1045
  file,
497
1046
  line,
498
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1047
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
499
1048
  };
500
1049
  }
501
- if (!Node.isObjectLiteralExpression(el))
1050
+ if (!ts3.isObjectLiteralExpression(el))
502
1051
  return;
503
1052
  const provideExpr = getProp(el, "provide");
504
1053
  if (!provideExpr)
@@ -513,26 +1062,36 @@ function parseProvider(el, exportsSet, ctx) {
513
1062
  const useExistingExpr = getProp(el, "useExisting");
514
1063
  if (useClassExpr) {
515
1064
  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();
1065
+ const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
1066
+ const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
1067
+ const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
519
1068
  let deps = explicitDeps;
520
1069
  let optionalDeps = [];
521
1070
  let selfDeps = [];
522
1071
  let skipSelfDeps = [];
523
1072
  let hostDeps = [];
524
- if (deps.length === 0 && cls) {
1073
+ let functionalInjects = [];
1074
+ if (cls) {
525
1075
  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;
1076
+ if (deps.length === 0) {
1077
+ deps = result.deps;
1078
+ optionalDeps = result.optionalDeps;
1079
+ selfDeps = result.selfDeps;
1080
+ skipSelfDeps = result.skipSelfDeps;
1081
+ hostDeps = result.hostDeps;
1082
+ } else {
1083
+ optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
1084
+ selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
1085
+ skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
1086
+ hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
1087
+ }
1088
+ functionalInjects = result.functionalInjects;
531
1089
  if (result.missing) {
532
1090
  warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
533
1091
  }
534
1092
  }
535
1093
  const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
1094
+ validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
536
1095
  return {
537
1096
  token,
538
1097
  tokenKind,
@@ -544,35 +1103,38 @@ function parseProvider(el, exportsSet, ctx) {
544
1103
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
545
1104
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
546
1105
  hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1106
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
547
1107
  multi: multi ?? undefined,
548
1108
  providedIn: injectable?.providedIn,
549
- hasOnDestroy: cls?.getMethod("onDestroy") !== undefined || undefined,
1109
+ hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
550
1110
  exported: exportsSet.has(token),
551
1111
  file,
552
1112
  line,
553
- importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().getFilePath()) : undefined
1113
+ importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
554
1114
  };
555
1115
  }
556
1116
  if (useValueExpr) {
1117
+ validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
557
1118
  return {
558
1119
  token,
559
1120
  tokenKind,
560
1121
  kind: "value",
561
- useValueExpr: useValueExpr.getText(),
1122
+ useValueExpr: nodeText(useValueExpr),
562
1123
  scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
563
1124
  deps: [],
564
1125
  multi: multi ?? undefined,
565
1126
  exported: exportsSet.has(token),
566
1127
  file,
567
1128
  line,
568
- importPath: Node.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
1129
+ importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
569
1130
  };
570
1131
  }
571
1132
  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();
1133
+ const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
1134
+ const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
1135
+ return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
1136
+ })() : nodeText(useFactoryExpr);
1137
+ validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
576
1138
  return {
577
1139
  token,
578
1140
  tokenKind,
@@ -584,11 +1146,12 @@ function parseProvider(el, exportsSet, ctx) {
584
1146
  exported: exportsSet.has(token),
585
1147
  file,
586
1148
  line,
587
- importPath: Node.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
1149
+ importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
588
1150
  };
589
1151
  }
590
1152
  if (useExistingExpr) {
591
1153
  const target = tokenNameOf(useExistingExpr, ctx).name;
1154
+ validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
592
1155
  return {
593
1156
  token,
594
1157
  tokenKind,
@@ -604,14 +1167,262 @@ function parseProvider(el, exportsSet, ctx) {
604
1167
  }
605
1168
  return;
606
1169
  }
1170
+ function expandProviderExpressions(expressions, ctx, seen = new Set) {
1171
+ const result = [];
1172
+ for (const expression of expressions) {
1173
+ if (ts3.isSpreadElement(expression)) {
1174
+ result.push(...expandProviderExpressions([expression.expression], ctx, seen));
1175
+ continue;
1176
+ }
1177
+ if (ts3.isIdentifier(expression)) {
1178
+ const declaration = resolveDeclaration(expression, ctx)[0];
1179
+ if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
1180
+ const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
1181
+ if (seen.has(key))
1182
+ continue;
1183
+ const initializer = declaration.initializer;
1184
+ if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
1185
+ const nested = initializer.arguments[0];
1186
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1187
+ seen.add(key);
1188
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1189
+ seen.delete(key);
1190
+ continue;
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
1196
+ const nested = expression.arguments[0];
1197
+ if (nested && ts3.isArrayLiteralExpression(nested)) {
1198
+ result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
1199
+ continue;
1200
+ }
1201
+ }
1202
+ result.push(expression);
1203
+ }
1204
+ return result;
1205
+ }
1206
+ function isProviderHelper(expression, name) {
1207
+ return nodeText(expression.expression).split(".").pop() === name;
1208
+ }
1209
+ function parseFunctionalProvider(expression, exportsSet, ctx) {
1210
+ if (!ts3.isCallExpression(expression))
1211
+ return;
1212
+ const helper = nodeText(expression.expression).split(".").pop();
1213
+ const args = expression.arguments;
1214
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1215
+ const line = lineOf(expression);
1216
+ if (helper === "provideToken") {
1217
+ const tokenExpr = args[0];
1218
+ const valueExpr = args[1];
1219
+ if (!tokenExpr || !valueExpr)
1220
+ return [];
1221
+ const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
1222
+ validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
1223
+ return [{
1224
+ token,
1225
+ tokenKind,
1226
+ kind: "value",
1227
+ useValueExpr: nodeText(valueExpr),
1228
+ scope: resolveScope({ tokenName: token }, ctx),
1229
+ deps: [],
1230
+ exported: exportsSet.has(token),
1231
+ file,
1232
+ line,
1233
+ importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
1234
+ }];
1235
+ }
1236
+ if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
1237
+ const initializer = args[0];
1238
+ if (!initializer)
1239
+ return [];
1240
+ const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
1241
+ return [{
1242
+ token,
1243
+ tokenKind: "injection-token",
1244
+ kind: "value",
1245
+ useValueExpr: nodeText(initializer),
1246
+ scope: "application",
1247
+ deps: [],
1248
+ multi: true,
1249
+ exported: false,
1250
+ file,
1251
+ line,
1252
+ importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
1253
+ }];
1254
+ }
1255
+ if (helper === "provideRouter") {
1256
+ const providers = [];
1257
+ const routes = args[0];
1258
+ if (routes) {
1259
+ providers.push({
1260
+ token: "ROUTE_CONFIG",
1261
+ tokenKind: "injection-token",
1262
+ kind: "value",
1263
+ useValueExpr: nodeText(routes),
1264
+ scope: "application",
1265
+ deps: [],
1266
+ exported: false,
1267
+ file,
1268
+ line,
1269
+ importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
1270
+ });
1271
+ }
1272
+ for (const feature of args.slice(1)) {
1273
+ if (!ts3.isCallExpression(feature))
1274
+ continue;
1275
+ const featureName = nodeText(feature.expression).split(".").pop();
1276
+ if (featureName === "withRouterConfig" && feature.arguments[0]) {
1277
+ providers.push({
1278
+ token: "ROUTER_CONFIGURATION",
1279
+ tokenKind: "injection-token",
1280
+ kind: "value",
1281
+ useValueExpr: nodeText(feature.arguments[0]),
1282
+ scope: "application",
1283
+ deps: [],
1284
+ exported: false,
1285
+ file,
1286
+ line
1287
+ });
1288
+ } else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
1289
+ const strategy = feature.arguments[0];
1290
+ const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
1291
+ providers.push({
1292
+ token: "TITLE_STRATEGY",
1293
+ tokenKind: "injection-token",
1294
+ kind: isClass ? "class" : "value",
1295
+ ...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
1296
+ scope: "application",
1297
+ deps: [],
1298
+ exported: false,
1299
+ file,
1300
+ line,
1301
+ importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
1302
+ });
1303
+ }
1304
+ }
1305
+ return providers;
1306
+ }
1307
+ if (helper === "provideHttpClient") {
1308
+ const providers = [{
1309
+ token: "HttpClient",
1310
+ tokenKind: "class",
1311
+ kind: "class",
1312
+ useClass: "HttpClient",
1313
+ scope: "application",
1314
+ deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1315
+ optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
1316
+ exported: false,
1317
+ file,
1318
+ line,
1319
+ importModule: "@supacloud/app"
1320
+ }];
1321
+ for (const feature of args) {
1322
+ if (!ts3.isCallExpression(feature))
1323
+ continue;
1324
+ const featureName = nodeText(feature.expression).split(".").pop();
1325
+ if (featureName === "withInterceptors") {
1326
+ for (const interceptorArg of feature.arguments) {
1327
+ const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
1328
+ for (const value of values) {
1329
+ providers.push({
1330
+ token: "HTTP_INTERCEPTORS",
1331
+ tokenKind: "injection-token",
1332
+ kind: "value",
1333
+ useValueExpr: nodeText(value),
1334
+ scope: "application",
1335
+ deps: [],
1336
+ multi: true,
1337
+ exported: false,
1338
+ file,
1339
+ line,
1340
+ importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
1341
+ });
1342
+ }
1343
+ }
1344
+ } else if (featureName === "withFetch" && feature.arguments.length > 0) {
1345
+ warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
1346
+ }
1347
+ }
1348
+ return providers;
1349
+ }
1350
+ return;
1351
+ }
1352
+ function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
1353
+ const expected = providerTokenValueType(provideExpr, ctx);
1354
+ const actual = providerImplementationType(implementationExpr, kind, ctx);
1355
+ if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
1356
+ return;
1357
+ if (ctx.checker.isTypeAssignableTo(actual, expected))
1358
+ return;
1359
+ const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
1360
+ ctx.diagnostics.push({
1361
+ severity: "error",
1362
+ code: "provider-type-mismatch",
1363
+ message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
1364
+ file,
1365
+ line,
1366
+ errorCode: "SC2010",
1367
+ docsUrl: "https://supacloud.dev/errors/SC2010"
1368
+ });
1369
+ }
1370
+ function providerTokenValueType(expr, ctx) {
1371
+ const type = ctx.checker.getTypeAtLocation(expr);
1372
+ const typeArguments = typeArgumentsOf(type, ctx);
1373
+ if (typeArguments.length > 0)
1374
+ return typeArguments[0];
1375
+ if (ts3.isIdentifier(expr)) {
1376
+ const declaration = resolveDeclaration(expr, ctx)[0];
1377
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1378
+ return declaredClassType(declaration, ctx);
1379
+ }
1380
+ }
1381
+ return;
1382
+ }
1383
+ function providerImplementationType(expr, kind, ctx) {
1384
+ if (kind === "class" || kind === "existing") {
1385
+ if (ts3.isIdentifier(expr)) {
1386
+ const declaration = resolveDeclaration(expr, ctx)[0];
1387
+ if (declaration && ts3.isClassDeclaration(declaration)) {
1388
+ return declaredClassType(declaration, ctx);
1389
+ }
1390
+ }
1391
+ const type = ctx.checker.getTypeAtLocation(expr);
1392
+ const typeArguments = typeArgumentsOf(type, ctx);
1393
+ return typeArguments.length > 0 ? typeArguments[0] : undefined;
1394
+ }
1395
+ if (kind === "factory") {
1396
+ const type = ctx.checker.getTypeAtLocation(expr);
1397
+ const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
1398
+ return signature?.getReturnType();
1399
+ }
1400
+ return ctx.checker.getTypeAtLocation(expr);
1401
+ }
1402
+ function declaredClassType(declaration, ctx) {
1403
+ const name = declaration.name;
1404
+ if (!name)
1405
+ return;
1406
+ const symbol = ctx.checker.getSymbolAtLocation(name);
1407
+ return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
1408
+ }
1409
+ function typeArgumentsOf(type, ctx) {
1410
+ return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
1411
+ }
1412
+ function isTypeReference(type) {
1413
+ return "target" in type;
1414
+ }
1415
+ function isUnknownOrAny(type) {
1416
+ return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
1417
+ }
607
1418
  function parseController(input, ctx) {
608
1419
  let decl;
609
- if (Node.isClassDeclaration(input)) {
1420
+ if (ts3.isClassDeclaration(input)) {
610
1421
  decl = input;
611
1422
  } else {
612
1423
  const unwrapped = unwrapForwardRef(input);
613
- const resolved = Node.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped)[0] : undefined;
614
- if (resolved && Node.isClassDeclaration(resolved)) {
1424
+ const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
1425
+ if (resolved && ts3.isClassDeclaration(resolved)) {
615
1426
  decl = resolved;
616
1427
  }
617
1428
  }
@@ -622,46 +1433,46 @@ function parseController(input, ctx) {
622
1433
  return;
623
1434
  let path = "/";
624
1435
  let standalone;
625
- const pathArg = controllerDec.getArguments()[0];
1436
+ const pathArg = decoratorArguments(controllerDec)[0];
626
1437
  if (pathArg) {
627
- if (Node.isStringLiteral(pathArg)) {
628
- path = pathArg.getLiteralText();
629
- } else if (Node.isObjectLiteralExpression(pathArg)) {
1438
+ if (ts3.isStringLiteral(pathArg)) {
1439
+ path = pathArg.text;
1440
+ } else if (ts3.isObjectLiteralExpression(pathArg)) {
630
1441
  const p = stringLiteralProp(pathArg, "path");
631
1442
  if (p)
632
1443
  path = p;
633
1444
  standalone = booleanProp(pathArg, "standalone");
634
1445
  }
635
1446
  }
636
- const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
637
- const file = sourcePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1447
+ const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
1448
+ const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
638
1449
  if (missing) {
639
- warn(ctx, "missing-deps", `controller ${decl.getName()} 的部分构造依赖无法静态解析`, file, decl.getStartLineNumber());
1450
+ warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
640
1451
  }
641
1452
  const injectable = parseInjectableOptions(decl, ctx);
642
1453
  const routes = [];
643
1454
  const schemaImports = {};
644
1455
  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));
1456
+ for (const dec of decoratorsOf(decl)) {
1457
+ if (decoratorName2(dec) === "UseGuards") {
1458
+ for (const gArg of decoratorArguments(dec)) {
1459
+ classGuards.push(tokenText(gArg, ctx));
649
1460
  }
650
1461
  }
651
1462
  }
652
- for (const method of decl.getMethods()) {
653
- for (const dec of method.getDecorators()) {
654
- const name = decoratorName(dec);
1463
+ for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
1464
+ for (const dec of decoratorsOf(method)) {
1465
+ const name = decoratorName2(dec);
655
1466
  const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
656
1467
  if (!httpMethod)
657
1468
  continue;
658
- const args = dec.getArguments();
1469
+ const args = decoratorArguments(dec);
659
1470
  const pathArg2 = args[0];
660
- const routePath = pathArg2 && Node.isStringLiteral(pathArg2) ? pathArg2.getLiteralText() : "/";
1471
+ const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
661
1472
  const route = {
662
1473
  method: httpMethod,
663
1474
  path: routePath,
664
- handler: method.getName()
1475
+ handler: propertyName(method.name)
665
1476
  };
666
1477
  const pathParams = [];
667
1478
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
@@ -679,13 +1490,13 @@ function parseController(input, ctx) {
679
1490
  const queryDefaults = {};
680
1491
  let hasBodyBinding = false;
681
1492
  const handlerParams = [];
682
- for (const p of method.getParameters()) {
683
- const pName = p.getName();
1493
+ for (const p of method.parameters) {
1494
+ const pName = parameterName(p);
684
1495
  let hasBindingDecorator = false;
685
1496
  let paramNode;
686
- for (const pDec of p.getDecorators()) {
687
- const dName = decoratorName(pDec);
688
- const dArgs = pDec.getArguments();
1497
+ for (const pDec of decoratorsOf(p)) {
1498
+ const dName = decoratorName2(pDec);
1499
+ const dArgs = decoratorArguments(pDec);
689
1500
  if (dName === "Param") {
690
1501
  hasBindingDecorator = true;
691
1502
  const parsed = parseBindingOptions(dArgs, pName);
@@ -727,7 +1538,7 @@ function parseController(input, ctx) {
727
1538
  }
728
1539
  if (!hasBindingDecorator && pathParams.includes(pName)) {
729
1540
  paramBindings.push(pName);
730
- const typeText = p.getType().getText();
1541
+ const typeText = p.type ? nodeText(p.type) : "";
731
1542
  let inferredTransform;
732
1543
  if (typeText === "number") {
733
1544
  paramTransforms[pName] = "number";
@@ -770,37 +1581,37 @@ function parseController(input, ctx) {
770
1581
  route.handlerParams = handlerParams;
771
1582
  const routeGuards = [...classGuards];
772
1583
  const routeCanDeactivate = [];
773
- for (const mDec of method.getDecorators()) {
774
- const dName = decoratorName(mDec);
775
- const mArgs = mDec.getArguments();
1584
+ for (const mDec of decoratorsOf(method)) {
1585
+ const dName = decoratorName2(mDec);
1586
+ const mArgs = decoratorArguments(mDec);
776
1587
  if (dName === "UseGuards") {
777
1588
  for (const gArg of mArgs) {
778
- routeGuards.push(tokenText(gArg));
1589
+ routeGuards.push(tokenText(gArg, ctx));
779
1590
  }
780
1591
  } else if (dName === "CanDeactivate") {
781
1592
  for (const gArg of mArgs) {
782
- routeCanDeactivate.push(tokenText(gArg));
1593
+ routeCanDeactivate.push(tokenText(gArg, ctx));
783
1594
  }
784
1595
  } else if (dName === "Title") {
785
1596
  const tArg = mArgs[0];
786
- if (tArg && Node.isStringLiteral(tArg)) {
787
- route.title = tArg.getLiteralText();
1597
+ if (tArg && ts3.isStringLiteral(tArg)) {
1598
+ route.title = tArg.text;
788
1599
  }
789
1600
  } else if (dName === "Data") {
790
1601
  const dArg = mArgs[0];
791
- if (dArg && Node.isObjectLiteralExpression(dArg)) {
1602
+ if (dArg && ts3.isObjectLiteralExpression(dArg)) {
792
1603
  route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
793
1604
  }
794
1605
  } else if (dName === "Resolve") {
795
1606
  const rArg = mArgs[0];
796
- if (rArg && Node.isObjectLiteralExpression(rArg)) {
1607
+ if (rArg && ts3.isObjectLiteralExpression(rArg)) {
797
1608
  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();
1609
+ for (const prop of rArg.properties) {
1610
+ if (ts3.isPropertyAssignment(prop)) {
1611
+ const rName = propertyName(prop.name);
1612
+ const init = prop.initializer;
802
1613
  if (init)
803
- resolvers[rName] = tokenText(init);
1614
+ resolvers[rName] = tokenText(init, ctx);
804
1615
  }
805
1616
  }
806
1617
  if (Object.keys(resolvers).length > 0) {
@@ -810,52 +1621,52 @@ function parseController(input, ctx) {
810
1621
  }
811
1622
  }
812
1623
  const optionsArg = args[1];
813
- if (optionsArg && Node.isObjectLiteralExpression(optionsArg)) {
1624
+ if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
814
1625
  for (const field of ["body", "params", "query", "response"]) {
815
1626
  const schemaExpr = getProp(optionsArg, field);
816
- if (schemaExpr && Node.isIdentifier(schemaExpr)) {
817
- route[field] = schemaExpr.getText();
1627
+ if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
1628
+ route[field] = nodeText(schemaExpr);
818
1629
  const importPath = importPathOf(schemaExpr, ctx);
819
1630
  if (importPath)
820
- schemaImports[schemaExpr.getText()] = importPath;
1631
+ schemaImports[schemaExpr.text] = importPath;
821
1632
  }
822
1633
  }
823
1634
  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();
1635
+ if (commandExpr && ts3.isIdentifier(commandExpr)) {
1636
+ const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
1637
+ route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
827
1638
  }
828
1639
  const guardsExpr = getProp(optionsArg, "guards");
829
- if (guardsExpr && Node.isArrayLiteralExpression(guardsExpr)) {
830
- for (const el of guardsExpr.getElements()) {
831
- routeGuards.push(tokenText(el));
1640
+ if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
1641
+ for (const el of guardsExpr.elements) {
1642
+ routeGuards.push(tokenText(el, ctx));
832
1643
  }
833
1644
  }
834
1645
  const canMatchExpr = getProp(optionsArg, "canMatch");
835
- if (canMatchExpr && Node.isArrayLiteralExpression(canMatchExpr)) {
1646
+ if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
836
1647
  const canMatchList = [];
837
- for (const el of canMatchExpr.getElements()) {
838
- canMatchList.push(tokenText(el));
1648
+ for (const el of canMatchExpr.elements) {
1649
+ canMatchList.push(tokenText(el, ctx));
839
1650
  }
840
1651
  if (canMatchList.length > 0) {
841
1652
  route.canMatch = canMatchList;
842
1653
  }
843
1654
  }
844
1655
  const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
845
- if (canDeactivateExpr && Node.isArrayLiteralExpression(canDeactivateExpr)) {
846
- for (const el of canDeactivateExpr.getElements()) {
847
- routeCanDeactivate.push(tokenText(el));
1656
+ if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
1657
+ for (const el of canDeactivateExpr.elements) {
1658
+ routeCanDeactivate.push(tokenText(el, ctx));
848
1659
  }
849
1660
  }
850
1661
  const resolversExpr = getProp(optionsArg, "resolvers");
851
- if (resolversExpr && Node.isObjectLiteralExpression(resolversExpr)) {
1662
+ if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
852
1663
  const resolvers = {};
853
- for (const prop of resolversExpr.getProperties()) {
854
- if (Node.isPropertyAssignment(prop)) {
855
- const rName = prop.getName();
856
- const init = prop.getInitializer();
1664
+ for (const prop of resolversExpr.properties) {
1665
+ if (ts3.isPropertyAssignment(prop)) {
1666
+ const rName = propertyName(prop.name);
1667
+ const init = prop.initializer;
857
1668
  if (init)
858
- resolvers[rName] = tokenText(init);
1669
+ resolvers[rName] = tokenText(init, ctx);
859
1670
  }
860
1671
  }
861
1672
  if (Object.keys(resolvers).length > 0) {
@@ -863,24 +1674,27 @@ function parseController(input, ctx) {
863
1674
  }
864
1675
  }
865
1676
  const redirectToExpr = getProp(optionsArg, "redirectTo");
866
- if (redirectToExpr && Node.isStringLiteral(redirectToExpr)) {
867
- route.redirectTo = redirectToExpr.getLiteralText();
1677
+ if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
1678
+ route.redirectTo = redirectToExpr.text;
868
1679
  }
869
1680
  const pathMatchExpr = getProp(optionsArg, "pathMatch");
870
- if (pathMatchExpr && Node.isStringLiteral(pathMatchExpr)) {
871
- const val = pathMatchExpr.getLiteralText();
1681
+ if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
1682
+ const val = pathMatchExpr.text;
872
1683
  if (val === "full" || val === "prefix") {
873
1684
  route.pathMatch = val;
874
1685
  }
875
1686
  }
876
1687
  const titleExpr = getProp(optionsArg, "title");
877
- if (titleExpr && Node.isStringLiteral(titleExpr)) {
878
- route.title = titleExpr.getLiteralText();
1688
+ if (titleExpr && ts3.isStringLiteral(titleExpr)) {
1689
+ route.title = titleExpr.text;
879
1690
  }
880
1691
  const dataExpr = getProp(optionsArg, "data");
881
- if (dataExpr && Node.isObjectLiteralExpression(dataExpr)) {
1692
+ if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
882
1693
  route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
883
1694
  }
1695
+ const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
1696
+ if (aspects.length > 0)
1697
+ route.aspects = aspects;
884
1698
  }
885
1699
  if (routeGuards.length > 0) {
886
1700
  route.guards = routeGuards;
@@ -892,46 +1706,40 @@ function parseController(input, ctx) {
892
1706
  }
893
1707
  }
894
1708
  return {
895
- className: decl.getName() ?? "<anonymous>",
1709
+ className: decl.name?.text ?? "<anonymous>",
896
1710
  path,
897
1711
  scope: injectable?.scope ?? "request",
898
1712
  deps,
1713
+ hasOnDestroy: hasDestroyHook(decl) || undefined,
899
1714
  optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
900
1715
  selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
901
1716
  skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
1717
+ hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
1718
+ functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
902
1719
  standalone: standalone || undefined,
903
1720
  routes,
904
1721
  file,
905
- importPath: modulePath(ctx.rootDir, decl.getSourceFile().getFilePath()),
1722
+ importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
906
1723
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
907
1724
  };
908
1725
  }
909
1726
  function classDeps(cls, ctx) {
910
1727
  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 = [];
1728
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1729
+ const deps = injectable?.deps ? [...injectable.deps] : [];
923
1730
  const optionalDeps = [];
924
1731
  const selfDeps = [];
925
1732
  const skipSelfDeps = [];
926
1733
  const hostDeps = [];
1734
+ const functionalInjects = [];
927
1735
  let missing = false;
928
- if (ctor && ctor.getParameters().length > 0) {
929
- const injectParams = parseInjectParams(cls);
1736
+ if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
1737
+ const injectParams = parseInjectParams(cls, ctx);
930
1738
  const optionalIndices = parseOptionalParams(cls);
931
1739
  const selfIndices = parseModifierParams(cls, "Self");
932
1740
  const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
933
1741
  const hostIndices = parseModifierParams(cls, "Host");
934
- ctor.getParameters().forEach((param, index) => {
1742
+ ctor.parameters.forEach((param, index) => {
935
1743
  const isOptional = optionalIndices.has(index);
936
1744
  const injected = injectParams.get(index);
937
1745
  const tokenName = injected ?? paramTypeTokenName(param, ctx);
@@ -951,47 +1759,62 @@ function classDeps(cls, ctx) {
951
1759
  }
952
1760
  });
953
1761
  }
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();
1762
+ for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
1763
+ const init = prop.initializer;
1764
+ if (init && ts3.isCallExpression(init)) {
1765
+ const callName = nodeText(init.expression).split(".").pop();
958
1766
  if (callName === "inject") {
959
- const [tokenArg, optionsArg] = init.getArguments();
1767
+ const [tokenArg, optionsArg] = init.arguments;
960
1768
  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
- }
1769
+ const tokenName = tokenText(tokenArg, ctx);
1770
+ const unwrappedToken = unwrapForwardRef(tokenArg);
1771
+ const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
1772
+ if (!known) {
1773
+ missing = true;
1774
+ continue;
1775
+ }
1776
+ if (!deps.includes(tokenName))
1777
+ deps.push(tokenName);
1778
+ const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
1779
+ optional: booleanProp(optionsArg, "optional") ?? false,
1780
+ self: booleanProp(optionsArg, "self") ?? false,
1781
+ skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
1782
+ host: booleanProp(optionsArg, "host") ?? false
1783
+ } : { optional: false, self: false, skipSelf: false, host: false };
1784
+ if (options.optional && !optionalDeps.includes(tokenName))
1785
+ optionalDeps.push(tokenName);
1786
+ if (options.self && !selfDeps.includes(tokenName))
1787
+ selfDeps.push(tokenName);
1788
+ if (options.skipSelf && !skipSelfDeps.includes(tokenName))
1789
+ skipSelfDeps.push(tokenName);
1790
+ if (options.host && !hostDeps.includes(tokenName))
1791
+ hostDeps.push(tokenName);
1792
+ if (!functionalInjects.some((entry) => entry.token === tokenName)) {
1793
+ functionalInjects.push({
1794
+ token: tokenName,
1795
+ expression: nodeText(unwrappedToken),
1796
+ importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
1797
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1798
+ return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
1799
+ })() : undefined,
1800
+ importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
1801
+ const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
1802
+ return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
1803
+ })() : undefined,
1804
+ ...options
1805
+ });
983
1806
  }
984
1807
  }
985
1808
  }
986
1809
  }
987
1810
  }
988
- return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
1811
+ return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
989
1812
  }
990
1813
  function paramTypeTokenName(param, ctx) {
991
- const typeNode = param.getTypeNode();
1814
+ const typeNode = param.type;
992
1815
  if (!typeNode)
993
1816
  return;
994
- const text = typeNode.getText().replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
1817
+ const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
995
1818
  if (ctx.classesByName.has(text))
996
1819
  return text;
997
1820
  if (ctx.tokensByName.has(text))
@@ -1009,63 +1832,63 @@ function parseInjectableOptions(cls, ctx) {
1009
1832
  const providedIn = stringLiteralProp(obj, "providedIn");
1010
1833
  const depsExpr = getProp(obj, "deps");
1011
1834
  return {
1012
- scope: scope && SCOPES.includes(scope) ? scope : undefined,
1835
+ scope: scope && isScope(scope) ? scope : undefined,
1013
1836
  providedIn: providedIn === "root" ? "root" : undefined,
1014
- deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el.getText()) : undefined
1837
+ deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
1015
1838
  };
1016
1839
  }
1017
- function parseInjectParams(cls) {
1840
+ function parseInjectParams(cls, ctx) {
1018
1841
  const result = new Map;
1019
- const ctor = cls.getConstructors()[0];
1842
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1020
1843
  if (!ctor)
1021
1844
  return result;
1022
- ctor.getParameters().forEach((param, index) => {
1023
- for (const dec of param.getDecorators()) {
1024
- if (decoratorName(dec) !== "Inject")
1845
+ ctor.parameters.forEach((param, index) => {
1846
+ for (const dec of decoratorsOf(param)) {
1847
+ if (decoratorName2(dec) !== "Inject")
1025
1848
  continue;
1026
- const arg = dec.getArguments()[0];
1849
+ const arg = decoratorArguments(dec)[0];
1027
1850
  if (arg)
1028
- result.set(index, tokenText(arg));
1851
+ result.set(index, tokenText(arg, ctx));
1029
1852
  }
1030
1853
  });
1031
1854
  return result;
1032
1855
  }
1033
1856
  function parseOptionalParams(cls) {
1034
1857
  const result = new Set;
1035
- const ctor = cls.getConstructors()[0];
1858
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1036
1859
  if (!ctor)
1037
1860
  return result;
1038
- ctor.getParameters().forEach((param, index) => {
1039
- for (const dec of param.getDecorators()) {
1040
- if (decoratorName(dec) === "Optional")
1861
+ ctor.parameters.forEach((param, index) => {
1862
+ for (const dec of decoratorsOf(param)) {
1863
+ if (decoratorName2(dec) === "Optional")
1041
1864
  result.add(index);
1042
1865
  }
1043
- if (param.hasQuestionToken())
1866
+ if (param.questionToken)
1044
1867
  result.add(index);
1045
1868
  });
1046
1869
  return result;
1047
1870
  }
1048
1871
  function parseModifierParams(cls, modifierName) {
1049
1872
  const result = new Set;
1050
- const ctor = cls.getConstructors()[0];
1873
+ const ctor = cls.members.find(ts3.isConstructorDeclaration);
1051
1874
  if (!ctor)
1052
1875
  return result;
1053
- ctor.getParameters().forEach((param, index) => {
1054
- for (const dec of param.getDecorators()) {
1055
- if (decoratorName(dec) === modifierName)
1876
+ ctor.parameters.forEach((param, index) => {
1877
+ for (const dec of decoratorsOf(param)) {
1878
+ if (decoratorName2(dec) === modifierName)
1056
1879
  result.add(index);
1057
1880
  }
1058
1881
  });
1059
1882
  return result;
1060
1883
  }
1061
1884
  function unwrapForwardRef(expr) {
1062
- if (Node.isCallExpression(expr)) {
1063
- const exprText = expr.getExpression().getText();
1885
+ if (ts3.isCallExpression(expr)) {
1886
+ const exprText = nodeText(expr.expression);
1064
1887
  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)) {
1888
+ const arg = expr.arguments[0];
1889
+ if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
1890
+ const body = arg.body;
1891
+ if (body && ts3.isExpression(body)) {
1069
1892
  return unwrapForwardRef(body);
1070
1893
  }
1071
1894
  }
@@ -1073,18 +1896,18 @@ function unwrapForwardRef(expr) {
1073
1896
  }
1074
1897
  return expr;
1075
1898
  }
1076
- function tokenText(expr) {
1899
+ function tokenText(expr, ctx) {
1077
1900
  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();
1901
+ if (ts3.isStringLiteral(unwrapped))
1902
+ return unwrapped.text;
1903
+ if (ts3.isIdentifier(unwrapped)) {
1904
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1905
+ if (decl && ts3.isClassDeclaration(decl))
1906
+ return decl.name?.text ?? unwrapped.text;
1907
+ if (decl && ts3.isVariableDeclaration(decl))
1908
+ return variableName(decl);
1086
1909
  }
1087
- return unwrapped.getText();
1910
+ return nodeText(unwrapped);
1088
1911
  }
1089
1912
  function resolveScope(input, ctx) {
1090
1913
  if (input.explicit)
@@ -1101,98 +1924,181 @@ function resolveScope(input, ctx) {
1101
1924
  }
1102
1925
  function tokenNameOf(expr, ctx) {
1103
1926
  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" };
1927
+ if (ts3.isIdentifier(unwrapped)) {
1928
+ const decl = resolveDeclaration(unwrapped, ctx)[0];
1929
+ if (decl && ts3.isClassDeclaration(decl)) {
1930
+ return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
1108
1931
  }
1109
- if (decl && Node.isVariableDeclaration(decl)) {
1110
- const name = decl.getName();
1932
+ if (decl && ts3.isVariableDeclaration(decl)) {
1933
+ const name = variableName(decl);
1111
1934
  return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
1112
1935
  }
1113
- if (ctx.tokensByName.has(expr.getText())) {
1114
- return { name: expr.getText(), kind: "injection-token" };
1936
+ if (ctx.tokensByName.has(unwrapped.text)) {
1937
+ return { name: unwrapped.text, kind: "injection-token" };
1115
1938
  }
1116
1939
  }
1117
- return { name: expr.getText(), kind: "class" };
1940
+ return { name: nodeText(expr), kind: "class" };
1118
1941
  }
1119
- function resolveDeclaration(id) {
1120
- let symbol = id.getSymbol();
1942
+ function resolveDeclaration(id, ctx) {
1943
+ let symbol = ctx.checker.getSymbolAtLocation(id);
1121
1944
  if (!symbol)
1122
1945
  return [];
1123
- let declarations = symbol.getDeclarations();
1946
+ let declarations = symbol.declarations ?? [];
1124
1947
  for (let guard = 0;guard < 4; guard += 1) {
1125
- const isAlias = declarations.some((d) => Node.isImportSpecifier(d) || Node.isImportClause(d) || Node.isNamespaceImport(d));
1948
+ const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
1126
1949
  if (!isAlias)
1127
1950
  break;
1128
- const aliased = symbol.getAliasedSymbol();
1129
- if (!aliased)
1951
+ if (!(symbol.flags & ts3.SymbolFlags.Alias))
1130
1952
  break;
1953
+ const aliased = ctx.checker.getAliasedSymbol(symbol);
1131
1954
  symbol = aliased;
1132
- declarations = aliased.getDeclarations();
1955
+ declarations = aliased.declarations ?? [];
1133
1956
  }
1134
1957
  return declarations;
1135
1958
  }
1136
1959
  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];
1960
+ const decl = resolveDeclaration(id, ctx)[0];
1146
1961
  if (decl)
1147
- return modulePath(ctx.rootDir, decl.getSourceFile().getFilePath());
1962
+ return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
1963
+ return;
1964
+ }
1965
+ function importModuleOf(id, ctx) {
1966
+ const symbol = ctx.checker.getSymbolAtLocation(id);
1967
+ const declarations = symbol?.declarations ?? [];
1968
+ for (const declaration of declarations) {
1969
+ let current = declaration;
1970
+ while (current) {
1971
+ if (ts3.isImportDeclaration(current)) {
1972
+ const moduleSpecifier = current.moduleSpecifier;
1973
+ return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
1974
+ }
1975
+ current = current.parent;
1976
+ }
1977
+ }
1148
1978
  return;
1149
1979
  }
1150
1980
  function findDecorator(cls, name) {
1151
- return cls.getDecorators().find((dec) => decoratorName(dec) === name);
1981
+ return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
1152
1982
  }
1153
- function decoratorName(dec) {
1154
- const expr = dec.getExpression();
1155
- if (Node.isCallExpression(expr)) {
1156
- return expr.getExpression().getText().split(".").pop();
1983
+ function decoratorName2(dec) {
1984
+ const expr = dec.expression;
1985
+ if (ts3.isCallExpression(expr)) {
1986
+ return nodeText(expr.expression).split(".").pop();
1157
1987
  }
1158
- if (Node.isIdentifier(expr))
1159
- return expr.getText();
1988
+ if (ts3.isIdentifier(expr))
1989
+ return expr.text;
1160
1990
  return;
1161
1991
  }
1162
1992
  function decoratorObjectArg(dec) {
1163
- const expr = dec.getExpression();
1164
- if (!Node.isCallExpression(expr))
1993
+ const expr = dec.expression;
1994
+ if (!ts3.isCallExpression(expr))
1165
1995
  return;
1166
- const arg = expr.getArguments()[0];
1167
- return arg && Node.isObjectLiteralExpression(arg) ? arg : undefined;
1996
+ const arg = expr.arguments[0];
1997
+ return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
1168
1998
  }
1169
1999
  function getProp(obj, name) {
1170
- const prop = obj.getProperty(name);
1171
- if (prop && Node.isPropertyAssignment(prop))
1172
- return prop.getInitializer();
2000
+ const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
2001
+ if (!prop)
2002
+ return;
2003
+ if (ts3.isPropertyAssignment(prop))
2004
+ return prop.initializer;
2005
+ if (ts3.isShorthandPropertyAssignment(prop))
2006
+ return prop.name;
1173
2007
  return;
1174
2008
  }
2009
+ function toCompilerDiagnostic(diagnostic, rootDir) {
2010
+ const file = diagnostic.file;
2011
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
2012
+ return {
2013
+ severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
2014
+ code: `typescript-${diagnostic.code}`,
2015
+ errorCode: `TS${diagnostic.code}`,
2016
+ message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
2017
+ `),
2018
+ file: file ? sourcePath(rootDir, file.fileName) : undefined,
2019
+ line: position ? position.line + 1 : undefined
2020
+ };
2021
+ }
1175
2022
  function stringLiteralProp(obj, name) {
1176
2023
  const expr = getProp(obj, name);
1177
- return expr && Node.isStringLiteral(expr) ? expr.getLiteralText() : undefined;
2024
+ return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
1178
2025
  }
1179
2026
  function arrayProp(obj, name) {
1180
2027
  const expr = getProp(obj, name);
1181
- return expr && Node.isArrayLiteralExpression(expr) ? expr.getElements() : [];
2028
+ return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
2029
+ }
2030
+ function parseAspectRefs(expression, ctx, owner) {
2031
+ if (!expression)
2032
+ return [];
2033
+ if (!ts3.isArrayLiteralExpression(expression)) {
2034
+ ctx.diagnostics.push({
2035
+ severity: "error",
2036
+ code: "dynamic-aspect-reference",
2037
+ message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
2038
+ file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
2039
+ line: lineOf(expression),
2040
+ suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
2041
+ errorCode: "SC4010",
2042
+ docsUrl: "https://supacloud.dev/errors/SC4010"
2043
+ });
2044
+ return [];
2045
+ }
2046
+ const refs = [];
2047
+ for (const element of expression.elements) {
2048
+ if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
2049
+ ctx.diagnostics.push({
2050
+ severity: "error",
2051
+ code: "dynamic-aspect-reference",
2052
+ message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
2053
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
2054
+ line: lineOf(element),
2055
+ suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
2056
+ errorCode: "SC4010",
2057
+ docsUrl: "https://supacloud.dev/errors/SC4010"
2058
+ });
2059
+ continue;
2060
+ }
2061
+ const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
2062
+ if (!declaration) {
2063
+ ctx.diagnostics.push({
2064
+ severity: "error",
2065
+ code: "invalid-aspect-reference",
2066
+ message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
2067
+ file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
2068
+ line: lineOf(element),
2069
+ suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
2070
+ errorCode: "SC4011",
2071
+ docsUrl: "https://supacloud.dev/errors/SC4011"
2072
+ });
2073
+ continue;
2074
+ }
2075
+ const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
2076
+ if (!name)
2077
+ continue;
2078
+ const declaredFile = declaration.getSourceFile().fileName;
2079
+ const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2080
+ refs.push({
2081
+ name,
2082
+ expression: element.text,
2083
+ importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
2084
+ importModule: projectLocal ? undefined : importModuleOf(element, ctx)
2085
+ });
2086
+ }
2087
+ return refs;
1182
2088
  }
1183
2089
  function booleanProp(obj, name) {
1184
2090
  const expr = getProp(obj, name);
1185
2091
  if (!expr)
1186
2092
  return;
1187
- if (expr.getKind() === SyntaxKind.TrueKeyword)
2093
+ if (expr.kind === ts3.SyntaxKind.TrueKeyword)
1188
2094
  return true;
1189
- if (expr.getKind() === SyntaxKind.FalseKeyword)
2095
+ if (expr.kind === ts3.SyntaxKind.FalseKeyword)
1190
2096
  return false;
1191
2097
  return;
1192
2098
  }
1193
2099
  function parseScopeProp(obj) {
1194
2100
  const scope = stringLiteralProp(obj, "scope");
1195
- return scope && SCOPES.includes(scope) ? scope : undefined;
2101
+ return scope && isScope(scope) ? scope : undefined;
1196
2102
  }
1197
2103
  function parseBindingOptions(args, defaultName) {
1198
2104
  let name = defaultName;
@@ -1200,16 +2106,16 @@ function parseBindingOptions(args, defaultName) {
1200
2106
  let defaultValue;
1201
2107
  const first = args[0];
1202
2108
  const second = args[1];
1203
- if (first && Node.isStringLiteral(first)) {
1204
- name = first.getLiteralText();
1205
- } else if (first && Node.isObjectLiteralExpression(first)) {
2109
+ if (first && ts3.isStringLiteral(first)) {
2110
+ name = first.text;
2111
+ } else if (first && ts3.isObjectLiteralExpression(first)) {
1206
2112
  const nameProp = getProp(first, "name");
1207
- if (nameProp && Node.isStringLiteral(nameProp)) {
1208
- name = nameProp.getLiteralText();
2113
+ if (nameProp && ts3.isStringLiteral(nameProp)) {
2114
+ name = nameProp.text;
1209
2115
  }
1210
2116
  const trProp = getProp(first, "transform");
1211
- if (trProp && Node.isStringLiteral(trProp)) {
1212
- const val = trProp.getLiteralText();
2117
+ if (trProp && ts3.isStringLiteral(trProp)) {
2118
+ const val = trProp.text;
1213
2119
  if (val === "number" || val === "boolean" || val === "string") {
1214
2120
  transform = val;
1215
2121
  }
@@ -1219,10 +2125,10 @@ function parseBindingOptions(args, defaultName) {
1219
2125
  defaultValue = parseLiteralValue(defProp);
1220
2126
  }
1221
2127
  }
1222
- if (second && Node.isObjectLiteralExpression(second)) {
2128
+ if (second && ts3.isObjectLiteralExpression(second)) {
1223
2129
  const trProp = getProp(second, "transform");
1224
- if (trProp && Node.isStringLiteral(trProp)) {
1225
- const val = trProp.getLiteralText();
2130
+ if (trProp && ts3.isStringLiteral(trProp)) {
2131
+ const val = trProp.text;
1226
2132
  if (val === "number" || val === "boolean" || val === "string") {
1227
2133
  transform = val;
1228
2134
  }
@@ -1235,28 +2141,28 @@ function parseBindingOptions(args, defaultName) {
1235
2141
  return { name, transform, default: defaultValue };
1236
2142
  }
1237
2143
  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")
2144
+ if (ts3.isStringLiteral(node))
2145
+ return node.text;
2146
+ if (ts3.isNumericLiteral(node))
2147
+ return Number(node.text);
2148
+ if (node.kind === ts3.SyntaxKind.TrueKeyword)
1243
2149
  return true;
1244
- if (node.getKindName() === "FalseKeyword")
2150
+ if (node.kind === ts3.SyntaxKind.FalseKeyword)
1245
2151
  return false;
1246
- if (Node.isArrayLiteralExpression(node)) {
1247
- return node.getElements().map(parseLiteralValue);
2152
+ if (ts3.isArrayLiteralExpression(node)) {
2153
+ return node.elements.map(parseLiteralValue);
1248
2154
  }
1249
- if (Node.isObjectLiteralExpression(node)) {
2155
+ if (ts3.isObjectLiteralExpression(node)) {
1250
2156
  return parseObjectLiteralValues(node);
1251
2157
  }
1252
2158
  return;
1253
2159
  }
1254
2160
  function parseObjectLiteralValues(obj) {
1255
2161
  const result = {};
1256
- for (const prop of obj.getProperties()) {
1257
- if (Node.isPropertyAssignment(prop)) {
1258
- const name = prop.getName();
1259
- const init = prop.getInitializer();
2162
+ for (const prop of obj.properties) {
2163
+ if (ts3.isPropertyAssignment(prop)) {
2164
+ const name = propertyName(prop.name);
2165
+ const init = prop.initializer;
1260
2166
  if (init) {
1261
2167
  result[name] = parseLiteralValue(init);
1262
2168
  }
@@ -1275,63 +2181,9 @@ function warn(ctx, code, message, file, line) {
1275
2181
  }
1276
2182
 
1277
2183
  // src/generate.ts
1278
- import { mkdir, rename, unlink, writeFile } from "node:fs/promises";
2184
+ import { createHash as createHash4 } from "node:crypto";
2185
+ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
1279
2186
  import { join as join2 } from "node:path";
1280
-
1281
- // src/util.ts
1282
- function camelName(token) {
1283
- const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
1284
- if (isConstantCase) {
1285
- return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1286
- }
1287
- return token.charAt(0).toLowerCase() + token.slice(1);
1288
- }
1289
- function relativeImportPath(fromDir, toFile) {
1290
- const fromParts = fromDir.split("/").filter(Boolean);
1291
- const toParts = toFile.split("/").filter(Boolean);
1292
- let common = 0;
1293
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
1294
- common += 1;
1295
- }
1296
- const ups = fromParts.length - common;
1297
- const downs = toParts.slice(common);
1298
- const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
1299
- const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
1300
- const joined = segments.join("/");
1301
- return joined.startsWith("..") ? joined : `./${joined}`;
1302
- }
1303
- var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
1304
- var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
1305
- function isRequestContextToken(token, tokenNames) {
1306
- return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
1307
- }
1308
- function isJobContextToken(token, tokenNames) {
1309
- return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
1310
- }
1311
- function joinRoutePaths(prefix, path) {
1312
- const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
1313
- const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
1314
- return normalized;
1315
- }
1316
- function findClosestMatch(target, candidates) {
1317
- if (candidates.length === 0)
1318
- return;
1319
- if (candidates.length === 1)
1320
- return candidates[0];
1321
- const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1322
- const targetNorm = norm(target);
1323
- for (const c of candidates) {
1324
- if (norm(c) === targetNorm)
1325
- return c;
1326
- }
1327
- for (const c of candidates) {
1328
- if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
1329
- return c;
1330
- }
1331
- return candidates[0];
1332
- }
1333
-
1334
- // src/generate.ts
1335
2187
  var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
1336
2188
  var INTERFACES = `export interface CompiledRoute {
1337
2189
  method: string;
@@ -1354,6 +2206,7 @@ var INTERFACES = `export interface CompiledRoute {
1354
2206
  queryDefaults?: Record<string, unknown>;
1355
2207
  title?: string;
1356
2208
  data?: Record<string, unknown>;
2209
+ aspects?: CompiledAspect[];
1357
2210
  invoker?: (
1358
2211
  controller: unknown,
1359
2212
  request: {
@@ -1374,8 +2227,33 @@ export interface CompiledCommand {
1374
2227
  audit?: string;
1375
2228
  idempotency: "required" | "none";
1376
2229
  standalone?: boolean;
2230
+ aspects?: CompiledAspect[];
2231
+ }
2232
+
2233
+ export interface CompiledJob {
2234
+ className: string;
2235
+ name: string;
2236
+ serviceKey: string;
2237
+ scope: "application" | "request" | "job";
2238
+ aspects?: CompiledAspect[];
1377
2239
  }
1378
2240
 
2241
+ export interface CompiledAspectContext {
2242
+ kind: "route" | "command" | "job";
2243
+ name: string;
2244
+ input: unknown;
2245
+ request?: Request;
2246
+ requestContext?: unknown;
2247
+ scope?: Record<string, unknown>;
2248
+ services?: Record<string, unknown>;
2249
+ metadata?: unknown;
2250
+ }
2251
+
2252
+ export type CompiledAspect = (
2253
+ context: CompiledAspectContext,
2254
+ next: () => unknown | Promise<unknown>,
2255
+ ) => unknown | Promise<unknown>;
2256
+
1379
2257
  export interface CompiledController {
1380
2258
  path: string;
1381
2259
  serviceKey: string;
@@ -1393,14 +2271,63 @@ export interface CompiledModule {
1393
2271
  services: Record<string, unknown>,
1394
2272
  ctx: unknown,
1395
2273
  imported?: Record<string, Record<string, unknown>>,
1396
- ): Record<string, unknown>;
2274
+ ): Promise<Record<string, unknown>>;
2275
+ destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
1397
2276
  createJobScope?(
1398
2277
  services: Record<string, unknown>,
1399
2278
  ctx: unknown,
1400
2279
  imported?: Record<string, Record<string, unknown>>,
1401
- ): Record<string, unknown>;
2280
+ ): Promise<Record<string, unknown>>;
2281
+ destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
1402
2282
  controllers: CompiledController[];
1403
2283
  commands: CompiledCommand[];
2284
+ jobs: CompiledJob[];
2285
+ aspects?: CompiledAspect[];
2286
+ }`;
2287
+ var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
2288
+ return typeof value === "object" && value !== null;
2289
+ }
2290
+
2291
+ function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
2292
+ return typeof value === "function";
2293
+ }
2294
+
2295
+ function resolveFactoryValue(value: unknown): unknown {
2296
+ if (!isRecord(value) || !isFunction(value.factory)) return undefined;
2297
+ return value.factory();
2298
+ }
2299
+
2300
+ const scopeDestructions = new WeakMap<object, Promise<void>>();
2301
+
2302
+ function destroyScopeInstances(
2303
+ scope: Record<string, unknown>,
2304
+ plan: readonly { key: string; index?: number }[],
2305
+ ): Promise<void> {
2306
+ const pending = scopeDestructions.get(scope);
2307
+ if (pending) return pending;
2308
+ const destruction = Promise.resolve().then(async () => {
2309
+ const errors: unknown[] = [];
2310
+ const seen = new Set<unknown>();
2311
+ for (const entry of [...plan].reverse()) {
2312
+ const value = scope[entry.key];
2313
+ const instance = entry.index === undefined ? value
2314
+ : Array.isArray(value) ? value[entry.index] : undefined;
2315
+ if (seen.has(instance)) continue;
2316
+ seen.add(instance);
2317
+ try {
2318
+ if (isRecord(instance) && isFunction(instance.onDestroy)) {
2319
+ await instance.onDestroy();
2320
+ } else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
2321
+ await instance.ngOnDestroy();
2322
+ }
2323
+ } catch (error) {
2324
+ errors.push(error);
2325
+ }
2326
+ }
2327
+ if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
2328
+ });
2329
+ scopeDestructions.set(scope, destruction);
2330
+ return destruction;
1404
2331
  }`;
1405
2332
  function renderApplication(graph, options) {
1406
2333
  let modules = topoSortModules(graph.modules);
@@ -1418,6 +2345,8 @@ function renderApplication(graph, options) {
1418
2345
  referencedTokens.add(d);
1419
2346
  for (const d of ctrl.skipSelfDeps ?? [])
1420
2347
  referencedTokens.add(d);
2348
+ for (const d of ctrl.hostDeps ?? [])
2349
+ referencedTokens.add(d);
1421
2350
  }
1422
2351
  for (const p of mod.providers) {
1423
2352
  for (const d of p.deps ?? [])
@@ -1428,6 +2357,8 @@ function renderApplication(graph, options) {
1428
2357
  referencedTokens.add(d);
1429
2358
  for (const d of p.skipSelfDeps ?? [])
1430
2359
  referencedTokens.add(d);
2360
+ for (const d of p.hostDeps ?? [])
2361
+ referencedTokens.add(d);
1431
2362
  if (p.useExisting)
1432
2363
  referencedTokens.add(p.useExisting);
1433
2364
  }
@@ -1452,6 +2383,8 @@ function renderApplication(graph, options) {
1452
2383
  ...imports.size > 0 ? [""] : [],
1453
2384
  INTERFACES,
1454
2385
  "",
2386
+ TYPE_GUARDS,
2387
+ "",
1455
2388
  "export function createCompiledModules(): CompiledModule[] {",
1456
2389
  " return [",
1457
2390
  ...descriptorEntries.map((entry) => indent(entry, 4) + ","),
@@ -1459,29 +2392,33 @@ function renderApplication(graph, options) {
1459
2392
  "}",
1460
2393
  "",
1461
2394
  "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();',
2395
+ ' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
2396
+ " for (const group of initializers) {",
2397
+ " if (Array.isArray(group)) {",
2398
+ " for (const init of group) {",
2399
+ " if (isFunction(init)) await init();",
2400
+ " }",
2401
+ " } else if (isFunction(group)) {",
2402
+ " await group();",
1466
2403
  " }",
1467
- ' } else if (typeof initializers === "function") {',
1468
- " await (initializers as () => unknown)();",
1469
2404
  " }",
1470
2405
  "}",
1471
2406
  "",
1472
2407
  "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") {',
2408
+ ' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
2409
+ " if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
1475
2410
  " await destroyRef.destroy();",
1476
- " } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
2411
+ " } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
1477
2412
  " for (const teardown of [...destroyRef._teardowns].reverse()) {",
1478
- ' if (typeof teardown === "function") await teardown();',
2413
+ " if (isFunction(teardown)) await teardown();",
1479
2414
  " }",
1480
2415
  " }",
1481
2416
  " const instances = Object.values(services);",
1482
2417
  " for (const inst of instances.reverse()) {",
1483
- ' if (inst && typeof (inst as any).onDestroy === "function") {',
1484
- " await (inst as any).onDestroy();",
2418
+ " if (isRecord(inst) && isFunction(inst.onDestroy)) {",
2419
+ " await inst.onDestroy();",
2420
+ " } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
2421
+ " await inst.ngOnDestroy();",
1485
2422
  " }",
1486
2423
  " }",
1487
2424
  "}",
@@ -1510,21 +2447,39 @@ async function generateApplication(graph, options) {
1510
2447
  await mkdir(options.outDir, { recursive: true });
1511
2448
  const applicationPath = join2(options.outDir, "application.ts");
1512
2449
  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];
2450
+ const written = [];
2451
+ if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
2452
+ written.push(applicationPath);
2453
+ }
2454
+ if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
2455
+ written.push(manifestPath);
2456
+ }
1516
2457
  if (rendered.clientCode) {
1517
2458
  const clientPath = join2(options.outDir, "client.ts");
1518
- await writeFileAtomic(clientPath, rendered.clientCode);
1519
- written.push(clientPath);
2459
+ if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
2460
+ written.push(clientPath);
2461
+ }
1520
2462
  }
1521
2463
  if (rendered.permissionsCode) {
1522
2464
  const permissionsPath = join2(options.outDir, "permissions.ts");
1523
- await writeFileAtomic(permissionsPath, rendered.permissionsCode);
1524
- written.push(permissionsPath);
2465
+ if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
2466
+ written.push(permissionsPath);
2467
+ }
1525
2468
  }
1526
2469
  return written;
1527
2470
  }
2471
+ async function writeFileIfChanged(path, content, hashes) {
2472
+ const hash = createHash4("sha1").update(content).digest("hex");
2473
+ if (hashes?.get(path) === hash) {
2474
+ try {
2475
+ await access(path);
2476
+ return false;
2477
+ } catch {}
2478
+ }
2479
+ await writeFileAtomic(path, content);
2480
+ hashes?.set(path, hash);
2481
+ return true;
2482
+ }
1528
2483
  async function writeFileAtomic(path, content) {
1529
2484
  const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1530
2485
  try {
@@ -1575,11 +2530,13 @@ class ImportManager {
1575
2530
  get size() {
1576
2531
  return this.entries.size;
1577
2532
  }
1578
- add(exported, importPath) {
1579
- if (!importPath)
2533
+ add(exported, importPath, importModule) {
2534
+ const path = importModule ?? importPath;
2535
+ const packageImport = importModule !== undefined;
2536
+ if (!path)
1580
2537
  return exported;
1581
2538
  for (const [local2, entry] of this.entries) {
1582
- if (entry.path === importPath && entry.exported === exported)
2539
+ if (entry.path === path && entry.exported === exported && entry.package === packageImport)
1583
2540
  return local2;
1584
2541
  }
1585
2542
  let local = exported;
@@ -1588,18 +2545,18 @@ class ImportManager {
1588
2545
  local = `${exported}${counter}`;
1589
2546
  counter += 1;
1590
2547
  }
1591
- this.entries.set(local, { path: importPath, exported });
2548
+ this.entries.set(local, { path, exported, package: packageImport });
1592
2549
  return local;
1593
2550
  }
1594
2551
  render(rootDir, outDir) {
1595
2552
  const byPath = new Map;
1596
2553
  for (const [local, entry] of this.entries) {
1597
- const list = byPath.get(entry.path) ?? [];
2554
+ const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
2555
+ const list = byPath.get(spec) ?? [];
1598
2556
  list.push({ exported: entry.exported, local });
1599
- byPath.set(entry.path, list);
2557
+ byPath.set(spec, list);
1600
2558
  }
1601
- return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, symbols]) => {
1602
- const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
2559
+ return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
1603
2560
  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
2561
  return `import { ${names} } from "${spec}";`;
1605
2562
  });
@@ -1621,14 +2578,19 @@ class ModuleGenerator {
1621
2578
  this.module = module;
1622
2579
  this.imports = imports;
1623
2580
  this.pascal = pascalName(module.name);
2581
+ if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
2582
+ imports.add("runInInjectionContext", undefined, "@supacloud/app");
2583
+ }
1624
2584
  }
1625
2585
  renderFactories() {
1626
2586
  const sections = [this.renderServicesFactory()];
1627
2587
  if (this.hasFactoryContent("request")) {
1628
2588
  sections.push(this.renderScopeFactory("request"));
2589
+ sections.push(this.renderScopeDestroyer("request"));
1629
2590
  }
1630
2591
  if (this.hasFactoryContent("job")) {
1631
2592
  sections.push(this.renderScopeFactory("job"));
2593
+ sections.push(this.renderScopeDestroyer("job"));
1632
2594
  }
1633
2595
  return sections;
1634
2596
  }
@@ -1640,12 +2602,18 @@ class ModuleGenerator {
1640
2602
  ];
1641
2603
  if (this.hasFactoryContent("request")) {
1642
2604
  lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
2605
+ lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
1643
2606
  }
1644
2607
  if (this.hasFactoryContent("job")) {
1645
2608
  lines.push(` createJobScope: create${this.pascal}JobScope,`);
2609
+ lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
1646
2610
  }
1647
2611
  lines.push(` controllers: ${this.renderControllers()},`);
1648
- lines.push(` commands: ${JSON.stringify(this.module.commands)},`);
2612
+ lines.push(` commands: ${this.renderCommands()},`);
2613
+ lines.push(` jobs: ${this.renderJobs()},`);
2614
+ if (this.module.aspects && this.module.aspects.length > 0) {
2615
+ lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
2616
+ }
1649
2617
  lines.push(`}`);
1650
2618
  return lines.join(`
1651
2619
  `);
@@ -1708,6 +2676,9 @@ class ModuleGenerator {
1708
2676
  if (route.data && Object.keys(route.data).length > 0) {
1709
2677
  fields.push(`data: ${JSON.stringify(route.data)}`);
1710
2678
  }
2679
+ if (route.aspects && route.aspects.length > 0) {
2680
+ fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2681
+ }
1711
2682
  const invokerArgs = (route.handlerParams ?? []).map((hp) => {
1712
2683
  if (hp.kind === "param") {
1713
2684
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
@@ -1746,7 +2717,7 @@ class ModuleGenerator {
1746
2717
  return "undefined";
1747
2718
  });
1748
2719
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
1749
- fields.push(`invoker: async (ctrl: any, req: any) => await (ctrl as any).${route.handler}(${callArgs})`);
2720
+ 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
2721
  return `{ ${fields.join(", ")} }`;
1751
2722
  });
1752
2723
  return [
@@ -1763,6 +2734,32 @@ class ModuleGenerator {
1763
2734
  ${indent(item, 2)}`).join(",")}
1764
2735
  ]`;
1765
2736
  }
2737
+ renderCommands() {
2738
+ if (this.module.commands.length === 0)
2739
+ return "[]";
2740
+ return `[${this.module.commands.map((command) => {
2741
+ const fields = [
2742
+ `className: ${JSON.stringify(command.className)}`,
2743
+ `name: ${JSON.stringify(command.name)}`,
2744
+ `permission: ${JSON.stringify(command.permission ?? "")}`,
2745
+ `transaction: ${JSON.stringify(command.transaction)}`,
2746
+ ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2747
+ `idempotency: ${JSON.stringify(command.idempotency)}`,
2748
+ ...command.standalone ? ["standalone: true"] : [],
2749
+ ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2750
+ ];
2751
+ return `{ ${fields.join(", ")} }`;
2752
+ }).join(", ")}]`;
2753
+ }
2754
+ renderJobs() {
2755
+ const jobs = this.module.jobs ?? [];
2756
+ if (jobs.length === 0)
2757
+ return "[]";
2758
+ return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(job.serviceKey)}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
2759
+ }
2760
+ renderAspects(aspects) {
2761
+ return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
2762
+ }
1766
2763
  renderServicesFactory() {
1767
2764
  return [
1768
2765
  `function create${this.pascal}Services(`,
@@ -1777,17 +2774,51 @@ ${indent(item, 2)}`).join(",")}
1777
2774
  renderScopeFactory(kind) {
1778
2775
  const suffix = kind === "request" ? "RequestScope" : "JobScope";
1779
2776
  return [
1780
- `function create${this.pascal}${suffix}(`,
2777
+ `async function create${this.pascal}${suffix}(`,
1781
2778
  ` services: Record<string, unknown>,`,
1782
2779
  ` ctx: unknown,`,
1783
2780
  ` imported: Record<string, Record<string, unknown>> = {},`,
1784
- `): Record<string, unknown> {`,
1785
- indent(this.renderFactoryBody(kind), 2),
2781
+ `): Promise<Record<string, unknown>> {`,
2782
+ ` const scope: Record<string, unknown> = {};`,
2783
+ ` try {`,
2784
+ indent(this.renderFactoryBody(kind, true), 4),
2785
+ ` } catch (error) {`,
2786
+ ` try {`,
2787
+ ` await destroy${this.pascal}${suffix}(scope);`,
2788
+ ` } catch (cleanupError) {`,
2789
+ ` console.error("supacloud: ${kind} scope rollback failed for ${this.module.name}", cleanupError);`,
2790
+ ` }`,
2791
+ ` throw error;`,
2792
+ ` }`,
2793
+ `}`
2794
+ ].join(`
2795
+ `);
2796
+ }
2797
+ renderScopeDestroyer(kind) {
2798
+ const suffix = kind === "request" ? "RequestScope" : "JobScope";
2799
+ const plan = [];
2800
+ const multiIndices = new Map;
2801
+ for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
2802
+ const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
2803
+ if (index !== undefined)
2804
+ multiIndices.set(provider.token, index + 1);
2805
+ if (provider.kind !== "existing" && provider.hasOnDestroy) {
2806
+ plan.push({ key: camelName(provider.token), index });
2807
+ }
2808
+ }
2809
+ for (const controller of this.module.controllers) {
2810
+ if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
2811
+ plan.push({ key: camelName(controller.className) });
2812
+ }
2813
+ }
2814
+ return [
2815
+ `async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
2816
+ ` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
1786
2817
  `}`
1787
2818
  ].join(`
1788
2819
  `);
1789
2820
  }
1790
- renderFactoryBody(kind) {
2821
+ renderFactoryBody(kind, scoped = false) {
1791
2822
  const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
1792
2823
  const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
1793
2824
  const lines = [];
@@ -1798,6 +2829,9 @@ ${indent(item, 2)}`).join(",")}
1798
2829
  const emitted = this.emitProvider(provider, kind, true);
1799
2830
  if (emitted.constLine)
1800
2831
  lines.push(emitted.constLine);
2832
+ if (scoped) {
2833
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = [...(Array.isArray(scope[${JSON.stringify(emitted.key)}]) ? scope[${JSON.stringify(emitted.key)}] : []), ${emitted.expr}];`);
2834
+ }
1801
2835
  const list = multiGroups.get(emitted.key) ?? [];
1802
2836
  list.push(emitted.expr);
1803
2837
  multiGroups.set(emitted.key, list);
@@ -1805,6 +2839,9 @@ ${indent(item, 2)}`).join(",")}
1805
2839
  const emitted = this.emitProvider(provider, kind, false);
1806
2840
  if (emitted.constLine)
1807
2841
  lines.push(emitted.constLine);
2842
+ if (scoped) {
2843
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
2844
+ }
1808
2845
  returns.set(emitted.key, emitted.expr);
1809
2846
  }
1810
2847
  }
@@ -1814,8 +2851,16 @@ ${indent(item, 2)}`).join(",")}
1814
2851
  for (const controller of controllers) {
1815
2852
  const emitted = this.emitController(controller, kind);
1816
2853
  lines.push(emitted.constLine);
2854
+ if (scoped) {
2855
+ lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
2856
+ }
1817
2857
  returns.set(emitted.key, emitted.expr);
1818
2858
  }
2859
+ if (scoped) {
2860
+ lines.push(`return scope;`);
2861
+ return lines.join(`
2862
+ `);
2863
+ }
1819
2864
  const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
1820
2865
  lines.push(`return { ${entries.join(", ")} };`);
1821
2866
  return lines.join(`
@@ -1825,39 +2870,76 @@ ${indent(item, 2)}`).join(",")}
1825
2870
  const key = camelName(provider.token);
1826
2871
  switch (provider.kind) {
1827
2872
  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(", ");
2873
+ const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
2874
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1830
2875
  const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
1831
- return { constLine: `const ${local} = new ${useClass}(${args});`, key, expr: local };
2876
+ return {
2877
+ constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
2878
+ key,
2879
+ expr: local
2880
+ };
1832
2881
  }
1833
2882
  case "value": {
1834
- const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
2883
+ const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
1835
2884
  const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
1836
2885
  return { constLine: `const ${local} = ${expr};`, key, expr: local };
1837
2886
  }
1838
2887
  case "factory": {
1839
2888
  if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
1840
- const tokenIdent = this.imports.add(provider.token, provider.importPath);
2889
+ const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
1841
2890
  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;`;
2891
+ const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
1843
2892
  return { constLine, key, expr: local2 };
1844
2893
  }
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(", ");
2894
+ const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2895
+ const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
1847
2896
  const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
1848
2897
  return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
1849
2898
  }
1850
2899
  case "existing": {
1851
- return { key, expr: this.depExpr(provider.useExisting ?? provider.token, kind, provider.optionalDeps?.includes(provider.token)) };
2900
+ return {
2901
+ key,
2902
+ expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
2903
+ };
1852
2904
  }
1853
2905
  }
1854
2906
  }
1855
2907
  emitController(controller, kind) {
1856
2908
  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(", ");
2909
+ const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
1858
2910
  const key = camelName(controller.className);
1859
2911
  const local = this.localVar(controller.className, kind);
1860
- return { constLine: `const ${local} = new ${className}(${args});`, key, expr: local };
2912
+ return {
2913
+ constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
2914
+ key,
2915
+ expr: local
2916
+ };
2917
+ }
2918
+ instantiate(className, args, kind, functionalInjects) {
2919
+ if (!functionalInjects || functionalInjects.length === 0) {
2920
+ return `new ${className}(${args})`;
2921
+ }
2922
+ const clauses = functionalInjects.map((entry) => {
2923
+ const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
2924
+ const value = this.depExpr(entry.token, kind, {
2925
+ optional: entry.optional,
2926
+ self: entry.self,
2927
+ skipSelf: entry.skipSelf,
2928
+ host: entry.host
2929
+ });
2930
+ return `if (token === ${token}) return ${value} as T;`;
2931
+ });
2932
+ const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
2933
+ const injector = [
2934
+ `{`,
2935
+ `get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
2936
+ ...clauses,
2937
+ missing,
2938
+ `},`,
2939
+ `}`
2940
+ ].join(`
2941
+ `);
2942
+ return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
1861
2943
  }
1862
2944
  localVar(token, kind) {
1863
2945
  const locals = this.locals[kind];
@@ -1874,23 +2956,34 @@ ${indent(item, 2)}`).join(",")}
1874
2956
  locals.set(token, local);
1875
2957
  return local;
1876
2958
  }
1877
- depExpr(token, kind, isOptional = false) {
2959
+ depOptions(node, token) {
2960
+ return {
2961
+ optional: node.optionalDeps?.includes(token) ?? false,
2962
+ self: node.selfDeps?.includes(token) ?? false,
2963
+ skipSelf: node.skipSelfDeps?.includes(token) ?? false,
2964
+ host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
2965
+ };
2966
+ }
2967
+ depExpr(token, kind, options = {}) {
2968
+ const isOptional = options.optional ?? false;
2969
+ const isSelf = options.self ?? false;
2970
+ const isSkipSelf = options.skipSelf ?? false;
1878
2971
  if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
1879
2972
  return "ctx";
1880
2973
  if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
1881
2974
  return "ctx";
1882
2975
  const own = this.module.providers.find((p) => p.token === token);
1883
- if (own) {
2976
+ const ownIsLocal = own && factoryOfScope(own.scope) === kind;
2977
+ if (own && ownIsLocal && !isSkipSelf) {
1884
2978
  if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
1885
2979
  return this.locals[kind].get(token) ?? camelName(token);
1886
2980
  }
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)}`;
2981
+ if (own.kind === "existing") {
2982
+ return this.depExpr(own.useExisting ?? token, kind, options);
1892
2983
  }
1893
- return `services.${camelName(token)}`;
2984
+ }
2985
+ if (isSelf) {
2986
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1894
2987
  }
1895
2988
  for (const importName of this.module.imports) {
1896
2989
  const imported = this.graph.modules.find((m) => m.name === importName);
@@ -1909,6 +3002,8 @@ ${indent(item, 2)}`).join(",")}
1909
3002
  if (isOptional && !this.graph.externalTokens.includes(token)) {
1910
3003
  return "undefined";
1911
3004
  }
3005
+ if (isSelf)
3006
+ return isOptional ? "undefined" : `services.${camelName(token)}`;
1912
3007
  if (kind === "services")
1913
3008
  return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
1914
3009
  return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
@@ -2366,12 +3461,17 @@ var COMPILER_DIAGNOSTIC_CODES = {
2366
3461
  "conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
2367
3462
  "missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
2368
3463
  "missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
3464
+ "provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
3465
+ "unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
2369
3466
  "command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
2370
3467
  "duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
2371
3468
  "route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
2372
3469
  "command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
2373
3470
  "route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
2374
3471
  "command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
3472
+ "invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
3473
+ "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3474
+ "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
2375
3475
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
2376
3476
  };
2377
3477
  function validateGraph(graph, options = false) {
@@ -2405,10 +3505,14 @@ function validateGraph(graph, options = false) {
2405
3505
  }
2406
3506
  }
2407
3507
  }
2408
- function resolveDep(module, token) {
2409
- const own = module.providers.find((p) => p.token === token);
2410
- if (own)
2411
- return { module, provider: own };
3508
+ function resolveDep(module, token, flags = {}) {
3509
+ if (!flags.skipSelf) {
3510
+ const own = module.providers.find((p) => p.token === token);
3511
+ if (own)
3512
+ return { module, provider: own };
3513
+ }
3514
+ if (flags.self)
3515
+ return;
2412
3516
  for (const importName of module.imports) {
2413
3517
  const imported = graph.modules.find((m) => m.name === importName);
2414
3518
  if (!imported || !imported.exports.includes(token))
@@ -2606,7 +3710,7 @@ function validateGraph(graph, options = false) {
2606
3710
  }
2607
3711
  if (controller.selfDeps && controller.selfDeps.length > 0) {
2608
3712
  for (const dep of controller.selfDeps) {
2609
- const own = module.providers.find((p) => p.token === dep);
3713
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2610
3714
  if (!own) {
2611
3715
  error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
2612
3716
  }
@@ -2614,7 +3718,7 @@ function validateGraph(graph, options = false) {
2614
3718
  }
2615
3719
  if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
2616
3720
  for (const dep of controller.skipSelfDeps) {
2617
- const own = module.providers.find((p) => p.token === dep);
3721
+ const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
2618
3722
  if (own) {
2619
3723
  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
3724
  }
@@ -2689,7 +3793,7 @@ function validateGraph(graph, options = false) {
2689
3793
  for (const provider of module.providers) {
2690
3794
  if (provider.selfDeps && provider.selfDeps.length > 0) {
2691
3795
  for (const dep of provider.selfDeps) {
2692
- const own = module.providers.find((p) => p.token === dep);
3796
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2693
3797
  if (!own) {
2694
3798
  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
3799
  }
@@ -2697,7 +3801,7 @@ function validateGraph(graph, options = false) {
2697
3801
  }
2698
3802
  if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
2699
3803
  for (const dep of provider.skipSelfDeps) {
2700
- const own = module.providers.find((p) => p.token === dep);
3804
+ const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
2701
3805
  if (own) {
2702
3806
  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
3807
  }
@@ -2705,7 +3809,10 @@ function validateGraph(graph, options = false) {
2705
3809
  }
2706
3810
  for (const dep of provider.deps) {
2707
3811
  const isOptional = provider.optionalDeps?.includes(dep);
2708
- const resolved = resolveDep(module, dep);
3812
+ const resolved = resolveDep(module, dep, {
3813
+ self: provider.selfDeps?.includes(dep),
3814
+ skipSelf: provider.skipSelfDeps?.includes(dep)
3815
+ });
2709
3816
  if (!resolved) {
2710
3817
  if (isOptional) {
2711
3818
  continue;
@@ -2713,6 +3820,8 @@ function validateGraph(graph, options = false) {
2713
3820
  if (!graph.externalTokens.includes(dep)) {
2714
3821
  if (globalProviders.has(dep)) {
2715
3822
  const owner = globalProviders.get(dep);
3823
+ if (!owner)
3824
+ continue;
2716
3825
  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
3826
  } else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
2718
3827
  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 +3882,8 @@ function validateGraph(graph, options = false) {
2773
3882
  }
2774
3883
  }
2775
3884
  if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
2776
- const hasAllowed = targetTags.some((t) => rule.onlyDependOnLibsWithTags.includes(t));
3885
+ const allowedTags = rule.onlyDependOnLibsWithTags;
3886
+ const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
2777
3887
  if (!hasAllowed && targetTags.length > 0) {
2778
3888
  error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
2779
3889
  }
@@ -2795,6 +3905,8 @@ function validateGraph(graph, options = false) {
2795
3905
  referencedTokens.add(d);
2796
3906
  for (const d of ctrl.skipSelfDeps ?? [])
2797
3907
  referencedTokens.add(d);
3908
+ for (const d of ctrl.hostDeps ?? [])
3909
+ referencedTokens.add(d);
2798
3910
  }
2799
3911
  for (const p of mod.providers) {
2800
3912
  for (const d of p.deps ?? [])
@@ -2805,6 +3917,8 @@ function validateGraph(graph, options = false) {
2805
3917
  referencedTokens.add(d);
2806
3918
  for (const d of p.skipSelfDeps ?? [])
2807
3919
  referencedTokens.add(d);
3920
+ for (const d of p.hostDeps ?? [])
3921
+ referencedTokens.add(d);
2808
3922
  if (p.useExisting)
2809
3923
  referencedTokens.add(p.useExisting);
2810
3924
  }
@@ -2927,7 +4041,10 @@ function detectCycles(graph, resolveDep) {
2927
4041
  state.set(id, "visiting");
2928
4042
  stack.push(ref);
2929
4043
  for (const dep of ref.provider.deps) {
2930
- const resolved = resolveDep(ref.module, dep);
4044
+ const resolved = resolveDep(ref.module, dep, {
4045
+ self: ref.provider.selfDeps?.includes(dep),
4046
+ skipSelf: ref.provider.skipSelfDeps?.includes(dep)
4047
+ });
2931
4048
  if (resolved)
2932
4049
  visit(resolved);
2933
4050
  }
@@ -3040,6 +4157,8 @@ function detectOrphanModules(graph) {
3040
4157
  }
3041
4158
  while (queue.length > 0) {
3042
4159
  const current = queue.shift();
4160
+ if (!current)
4161
+ continue;
3043
4162
  const mod = moduleMap.get(current);
3044
4163
  if (!mod)
3045
4164
  continue;
@@ -3068,10 +4187,225 @@ function detectOrphanModules(graph) {
3068
4187
  }
3069
4188
 
3070
4189
  // src/compile.ts
3071
- import { existsSync as existsSync2, readFileSync } from "node:fs";
3072
- import { join as join3 } from "node:path";
4190
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
4191
+ import { join as join4 } from "node:path";
4192
+
4193
+ // src/type-safety.ts
4194
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
4195
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4196
+ import * as ts4 from "@typescript/typescript6";
4197
+ var DEFAULT_EXCLUDES = [
4198
+ "**/*.test.ts",
4199
+ "**/*.spec.ts",
4200
+ "**/test/**",
4201
+ "**/tests/**",
4202
+ "**/__tests__/**",
4203
+ "**/fixtures/**",
4204
+ "**/generated/**",
4205
+ "**/dist/**",
4206
+ "**/*.d.ts"
4207
+ ];
4208
+ var DIAGNOSTIC_META = {
4209
+ "generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
4210
+ "source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
4211
+ "source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
4212
+ "source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
4213
+ "source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
4214
+ };
4215
+ function scanGeneratedArtifacts(artifacts, strict = true) {
4216
+ const diagnostics = [];
4217
+ for (const [file, content] of Object.entries(artifacts)) {
4218
+ if (content === undefined)
4219
+ continue;
4220
+ const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
4221
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4222
+ diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
4223
+ }
4224
+ }
4225
+ return diagnostics;
4226
+ }
4227
+ function scanProductionSource(options) {
4228
+ const rootDir = resolve2(options.rootDir);
4229
+ const configPath = join3(rootDir, "tsconfig.json");
4230
+ const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
4231
+ options: {
4232
+ strict: true,
4233
+ skipLibCheck: true,
4234
+ target: ts4.ScriptTarget.ES2022,
4235
+ module: ts4.ModuleKind.ESNext
4236
+ },
4237
+ errors: []
4238
+ };
4239
+ const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
4240
+ const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
4241
+ const compilerOptions = { ...projectConfig.options, noEmit: true };
4242
+ const host = ts4.createCompilerHost(compilerOptions);
4243
+ host.getCurrentDirectory = () => rootDir;
4244
+ const program = ts4.createProgram(rootNames, compilerOptions, host);
4245
+ const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
4246
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
4247
+ const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
4248
+ const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
4249
+ severity: "error",
4250
+ code: "source-config",
4251
+ message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
4252
+ `),
4253
+ file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
4254
+ line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
4255
+ errorCode: `TS${diagnostic.code}`
4256
+ }));
4257
+ const checker = program.getTypeChecker();
4258
+ for (const sourceFile of sourceFiles) {
4259
+ scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
4260
+ }
4261
+ return diagnostics;
4262
+ }
4263
+ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
4264
+ for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
4265
+ diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
4266
+ }
4267
+ for (const node of descendants(sourceFile)) {
4268
+ if (ts4.isAsExpression(node)) {
4269
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4270
+ continue;
4271
+ const assertedType = node.type.getText(sourceFile);
4272
+ if (assertedType === "const")
4273
+ continue;
4274
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4275
+ } else if (ts4.isTypeAssertionExpression(node)) {
4276
+ if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
4277
+ continue;
4278
+ diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
4279
+ } else if (ts4.isNonNullExpression(node)) {
4280
+ diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
4281
+ }
4282
+ }
4283
+ for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
4284
+ const initializer = declaration.initializer;
4285
+ if (!initializer || declaration.type)
4286
+ continue;
4287
+ const declarationType = checker.getTypeAtLocation(declaration.name);
4288
+ const initializerType = checker.getTypeAtLocation(initializer);
4289
+ for (const name of bindingNames(declaration.name)) {
4290
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4291
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
4292
+ }
4293
+ }
4294
+ if (isAnyType(declarationType))
4295
+ continue;
4296
+ if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
4297
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
4298
+ }
4299
+ 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))) {
4300
+ diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
4301
+ }
4302
+ }
4303
+ for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
4304
+ if (parameter.type)
4305
+ continue;
4306
+ for (const name of bindingNames(parameter.name)) {
4307
+ if (isAnyType(checker.getTypeAtLocation(name))) {
4308
+ diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
4309
+ }
4310
+ }
4311
+ }
4312
+ }
4313
+ function readProjectConfig2(configPath) {
4314
+ const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
4315
+ if (config.error)
4316
+ return { options: {}, errors: [config.error] };
4317
+ const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
4318
+ return { options: parsed.options, errors: parsed.errors };
4319
+ }
4320
+ function isProductionSource(rootDir, sourceFile, excludes, outDir) {
4321
+ const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
4322
+ if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
4323
+ return false;
4324
+ if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
4325
+ return false;
4326
+ return !excludes.some((pattern) => globMatches(relativePath, pattern));
4327
+ }
4328
+ function isProductionSourcePath(rootDir, filePath, excludes) {
4329
+ const relativePath = normalizeRelative(rootDir, filePath);
4330
+ return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
4331
+ }
4332
+ function globMatches(value, pattern) {
4333
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
4334
+ return new RegExp(`^${escaped}$`).test(value);
4335
+ }
4336
+ function bindingNames(name) {
4337
+ if (ts4.isIdentifier(name))
4338
+ return [name];
4339
+ return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
4340
+ }
4341
+ function isLiteralExpression(node) {
4342
+ if (!node)
4343
+ return false;
4344
+ return [
4345
+ ts4.SyntaxKind.StringLiteral,
4346
+ ts4.SyntaxKind.NumericLiteral,
4347
+ ts4.SyntaxKind.TrueKeyword,
4348
+ ts4.SyntaxKind.FalseKeyword
4349
+ ].includes(node.kind);
4350
+ }
4351
+ function isLiteralSyntax(node) {
4352
+ return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
4353
+ }
4354
+ function isLiteralType(type) {
4355
+ return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
4356
+ }
4357
+ function isAnyType(type) {
4358
+ return (type.flags & ts4.TypeFlags.Any) !== 0;
4359
+ }
4360
+ function isLetDeclaration(declaration) {
4361
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4362
+ }
4363
+ function isConstDeclaration(declaration) {
4364
+ return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
4365
+ }
4366
+ function descendants(root) {
4367
+ const result = [];
4368
+ const visit = (node) => {
4369
+ result.push(node);
4370
+ ts4.forEachChild(node, visit);
4371
+ };
4372
+ ts4.forEachChild(root, visit);
4373
+ return result;
4374
+ }
4375
+ function descendantsOfKind2(root, predicate) {
4376
+ const result = [];
4377
+ const visit = (node) => {
4378
+ if (predicate(node))
4379
+ result.push(node);
4380
+ ts4.forEachChild(node, visit);
4381
+ };
4382
+ ts4.forEachChild(root, visit);
4383
+ return result;
4384
+ }
4385
+ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
4386
+ const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
4387
+ const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
4388
+ const meta = DIAGNOSTIC_META[code];
4389
+ return {
4390
+ severity: strict ? "error" : "warn",
4391
+ code,
4392
+ message,
4393
+ file,
4394
+ line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
4395
+ errorCode: meta.errorCode,
4396
+ docsUrl: meta.docsUrl
4397
+ };
4398
+ }
4399
+ function normalizeRelative(rootDir, filePath) {
4400
+ return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4401
+ }
4402
+ function isAnyKeyword(node) {
4403
+ return node.kind === ts4.SyntaxKind.AnyKeyword;
4404
+ }
4405
+
4406
+ // src/compile.ts
3073
4407
  async function compileProject(options) {
3074
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4408
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3075
4409
  const diagnostics = [
3076
4410
  ...graph.diagnostics ?? [],
3077
4411
  ...validateGraph(graph, {
@@ -3090,14 +4424,40 @@ async function compileProject(options) {
3090
4424
  diagnostic.severity = "error";
3091
4425
  }
3092
4426
  }
3093
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
3094
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, {
4427
+ const typeSafety = resolveTypeSafety(options);
4428
+ const rendered = renderApplication(graph, {
3095
4429
  rootDir: options.rootDir,
3096
4430
  outDir: options.outDir,
3097
4431
  generateClient: options.generateClient,
3098
4432
  generatePermissions: options.generatePermissions,
3099
4433
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3100
- }) : [];
4434
+ });
4435
+ if (typeSafety.scanProductionSource) {
4436
+ diagnostics.push(...scanProductionSource({
4437
+ rootDir: options.rootDir,
4438
+ include: options.include,
4439
+ outDir: options.outDir,
4440
+ strict: options.strict,
4441
+ ...typeSafety
4442
+ }));
4443
+ }
4444
+ if (typeSafety.noAnyInGenerated) {
4445
+ diagnostics.push(...scanGeneratedArtifacts({
4446
+ "application.ts": rendered.applicationCode,
4447
+ "client.ts": rendered.clientCode,
4448
+ "permissions.ts": rendered.permissionsCode
4449
+ }, options.strict ?? false));
4450
+ }
4451
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
4452
+ const generatedOptions = {
4453
+ rootDir: options.rootDir,
4454
+ outDir: options.outDir,
4455
+ generateClient: options.generateClient,
4456
+ generatePermissions: options.generatePermissions,
4457
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4458
+ artifactHashes: options.cache?.generatedHashes
4459
+ };
4460
+ const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
3101
4461
  const stats = graph.cacheStats ? {
3102
4462
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
3103
4463
  changedFiles: [],
@@ -3108,7 +4468,7 @@ async function compileProject(options) {
3108
4468
  return { diagnostics, graph, written, stats };
3109
4469
  }
3110
4470
  async function checkProject(options) {
3111
- const graph = await analyzeProject(options.rootDir, options.include, options.cache);
4471
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
3112
4472
  const diagnostics = [
3113
4473
  ...graph.diagnostics ?? [],
3114
4474
  ...validateGraph(graph, {
@@ -3127,6 +4487,7 @@ async function checkProject(options) {
3127
4487
  diagnostic.severity = "error";
3128
4488
  }
3129
4489
  }
4490
+ const typeSafety = resolveTypeSafety(options);
3130
4491
  const rendered = renderApplication(graph, {
3131
4492
  rootDir: options.rootDir,
3132
4493
  outDir: options.outDir,
@@ -3134,6 +4495,22 @@ async function checkProject(options) {
3134
4495
  generatePermissions: options.generatePermissions,
3135
4496
  treeShakeUnusedProviders: options.treeShakeUnusedProviders
3136
4497
  });
4498
+ if (typeSafety.scanProductionSource) {
4499
+ diagnostics.push(...scanProductionSource({
4500
+ rootDir: options.rootDir,
4501
+ include: options.include,
4502
+ outDir: options.outDir,
4503
+ strict: options.strict,
4504
+ ...typeSafety
4505
+ }));
4506
+ }
4507
+ if (typeSafety.noAnyInGenerated) {
4508
+ diagnostics.push(...scanGeneratedArtifacts({
4509
+ "application.ts": rendered.applicationCode,
4510
+ "client.ts": rendered.clientCode,
4511
+ "permissions.ts": rendered.permissionsCode
4512
+ }, options.strict ?? false));
4513
+ }
3137
4514
  const expectedFiles = {
3138
4515
  "application.ts": rendered.applicationCode,
3139
4516
  "app.manifest.json": rendered.manifestJson
@@ -3146,12 +4523,12 @@ async function checkProject(options) {
3146
4523
  }
3147
4524
  const mismatches = [];
3148
4525
  for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
3149
- const diskPath = join3(options.outDir, filename);
3150
- if (!existsSync2(diskPath)) {
4526
+ const diskPath = join4(options.outDir, filename);
4527
+ if (!existsSync3(diskPath)) {
3151
4528
  mismatches.push(`${filename}: generated artifact is missing from disk`);
3152
4529
  continue;
3153
4530
  }
3154
- const diskContent = readFileSync(diskPath, "utf8");
4531
+ const diskContent = readFileSync3(diskPath, "utf8");
3155
4532
  if (diskContent !== expectedContent) {
3156
4533
  mismatches.push(`${filename}: disk artifact differs from current compiler output`);
3157
4534
  }
@@ -3163,10 +4540,17 @@ async function checkProject(options) {
3163
4540
  graph
3164
4541
  };
3165
4542
  }
4543
+ function resolveTypeSafety(options) {
4544
+ return {
4545
+ noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
4546
+ scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
4547
+ exclude: options.typeSafety?.exclude
4548
+ };
4549
+ }
3166
4550
 
3167
4551
  // src/inspect.ts
3168
- import { existsSync as existsSync3 } from "node:fs";
3169
- import { join as join4 } from "node:path";
4552
+ import { existsSync as existsSync4 } from "node:fs";
4553
+ import { join as join5 } from "node:path";
3170
4554
  function formatGraph(graph) {
3171
4555
  const lines = [];
3172
4556
  for (const module of graph.modules) {
@@ -3207,13 +4591,13 @@ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
3207
4591
  const checks = [
3208
4592
  {
3209
4593
  name: "project-root",
3210
- ok: existsSync3(rootDir),
3211
- detail: existsSync3(rootDir) ? rootDir : `missing: ${rootDir}`
4594
+ ok: existsSync4(rootDir),
4595
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
3212
4596
  },
3213
4597
  {
3214
4598
  name: "tsconfig",
3215
- ok: existsSync3(join4(rootDir, "tsconfig.json")),
3216
- detail: existsSync3(join4(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
4599
+ ok: existsSync4(join5(rootDir, "tsconfig.json")),
4600
+ detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
3217
4601
  },
3218
4602
  {
3219
4603
  name: "modules",
@@ -3303,27 +4687,31 @@ function exportGraphDot(graph) {
3303
4687
 
3304
4688
  // src/watch.ts
3305
4689
  import { watch } from "node:fs";
3306
- import { relative as relative3, resolve as resolve2 } from "node:path";
4690
+ import { relative as relative4, resolve as resolve4 } from "node:path";
3307
4691
 
3308
4692
  // 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";
4693
+ import { createHash as createHash5 } from "node:crypto";
4694
+ import { access as access2, readdir, readFile } from "node:fs/promises";
4695
+ import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
3312
4696
  function createDependencyGraphCache() {
3313
4697
  return {
3314
4698
  modules: new Map,
3315
- fileHashes: new Map
4699
+ fileHashes: new Map,
4700
+ generatedHashes: new Map
3316
4701
  };
3317
4702
  }
3318
4703
  function createIncrementalCompiler() {
3319
4704
  let previousSnapshot;
3320
4705
  let previousResult;
4706
+ let previousCache;
3321
4707
  const cache = createDependencyGraphCache();
3322
4708
  return {
3323
4709
  async compile(options, changedPaths) {
3324
- const snapshot = changedPaths && previousSnapshot ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
4710
+ const optionsKey = optionsKeyOf(options);
4711
+ const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
3325
4712
  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);
4713
+ const activeCache = options.cache ?? cache;
4714
+ const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
3327
4715
  if (cacheHit && previousResult) {
3328
4716
  return {
3329
4717
  ...previousResult,
@@ -3336,22 +4724,14 @@ function createIncrementalCompiler() {
3336
4724
  }
3337
4725
  };
3338
4726
  }
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
4727
  if (!activeCache.dependencyGraph && previousResult) {
3352
4728
  activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
3353
4729
  }
3354
- const result = await compileProject({ ...options, cache: activeCache });
4730
+ const result = await compileProject({
4731
+ ...options,
4732
+ cache: activeCache,
4733
+ changedPaths: changedFiles
4734
+ });
3355
4735
  const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
3356
4736
  const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
3357
4737
  const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
@@ -3367,14 +4747,18 @@ function createIncrementalCompiler() {
3367
4747
  };
3368
4748
  previousSnapshot = snapshot;
3369
4749
  previousResult = result;
4750
+ previousCache = activeCache;
3370
4751
  return { ...result, stats };
3371
4752
  },
3372
4753
  reset() {
3373
4754
  previousSnapshot = undefined;
3374
4755
  previousResult = undefined;
4756
+ previousCache = undefined;
3375
4757
  cache.modules.clear();
3376
4758
  cache.fileHashes.clear();
4759
+ cache.generatedHashes?.clear();
3377
4760
  cache.dependencyGraph = undefined;
4761
+ cache.programSession?.reset();
3378
4762
  },
3379
4763
  getCache() {
3380
4764
  return cache;
@@ -3382,18 +4766,21 @@ function createIncrementalCompiler() {
3382
4766
  };
3383
4767
  }
3384
4768
  async function updateSnapshot(previous, options, changedPaths) {
3385
- const rootDir = resolve(options.rootDir);
3386
- const outDir = resolve(options.outDir);
4769
+ const rootDir = resolve3(options.rootDir);
4770
+ const outDir = resolve3(options.outDir);
3387
4771
  const files = { ...previous.files };
3388
4772
  for (const changedPath of changedPaths) {
3389
- const absolutePath = resolve(rootDir, changedPath);
4773
+ const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4774
+ const relativeChangedPath = relative3(rootDir, absolutePath);
4775
+ if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
4776
+ continue;
3390
4777
  if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
3391
4778
  continue;
3392
- const relativePath = relative2(rootDir, absolutePath).split(sep2).join("/");
4779
+ const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
3393
4780
  try {
3394
- await access(absolutePath);
4781
+ await access2(absolutePath);
3395
4782
  const content = await readFile(absolutePath);
3396
- files[relativePath] = createHash2("sha256").update(content).digest("hex");
4783
+ files[relativePath] = createHash5("sha256").update(content).digest("hex");
3397
4784
  } catch {
3398
4785
  delete files[relativePath];
3399
4786
  }
@@ -3401,20 +4788,23 @@ async function updateSnapshot(previous, options, changedPaths) {
3401
4788
  return { files, optionsKey: optionsKeyOf(options) };
3402
4789
  }
3403
4790
  async function createSnapshot(options) {
3404
- const rootDir = resolve(options.rootDir);
3405
- const outDir = resolve(options.outDir);
4791
+ const rootDir = resolve3(options.rootDir);
4792
+ const outDir = resolve3(options.outDir);
3406
4793
  const paths = await listSourceFiles(rootDir, outDir);
3407
4794
  const files = {};
3408
4795
  for (const path of paths) {
3409
4796
  const content = await readFile(path);
3410
- files[relative2(rootDir, path).split(sep2).join("/")] = createHash2("sha256").update(content).digest("hex");
4797
+ files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
3411
4798
  }
3412
4799
  return { files, optionsKey: optionsKeyOf(options) };
3413
4800
  }
3414
4801
  function optionsKeyOf(options) {
3415
4802
  return JSON.stringify({
4803
+ rootDir: resolve3(options.rootDir),
4804
+ outDir: resolve3(options.outDir),
3416
4805
  include: options.include,
3417
4806
  strict: options.strict,
4807
+ writeOnError: options.writeOnError,
3418
4808
  moduleBoundaryPreset: options.moduleBoundaryPreset,
3419
4809
  moduleBoundaries: options.moduleBoundaries,
3420
4810
  allowRouteCommandBindings: options.allowRouteCommandBindings,
@@ -3422,27 +4812,16 @@ function optionsKeyOf(options) {
3422
4812
  disallowControllerDirectDb: options.disallowControllerDirectDb,
3423
4813
  detectOrphanModules: options.detectOrphanModules,
3424
4814
  generateClient: options.generateClient,
3425
- generatePermissions: options.generatePermissions
4815
+ generatePermissions: options.generatePermissions,
4816
+ typeSafety: options.typeSafety,
4817
+ treeShakeUnusedProviders: options.treeShakeUnusedProviders
3426
4818
  });
3427
4819
  }
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
4820
  async function listSourceFiles(rootDir, outDir) {
3442
4821
  const result = [];
3443
4822
  const visit = async (directory) => {
3444
4823
  for (const entry of await readdir(directory, { withFileTypes: true })) {
3445
- const path = resolve(directory, entry.name);
4824
+ const path = resolve3(directory, entry.name);
3446
4825
  if (entry.isDirectory()) {
3447
4826
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
3448
4827
  continue;
@@ -3500,7 +4879,9 @@ class ModuleDependencyGraph {
3500
4879
  if (!this.dependents.has(imp)) {
3501
4880
  this.dependents.set(imp, new Set);
3502
4881
  }
3503
- this.dependents.get(imp).add(modName);
4882
+ const dependents = this.dependents.get(imp);
4883
+ if (dependents)
4884
+ dependents.add(modName);
3504
4885
  }
3505
4886
  }
3506
4887
  }
@@ -3511,7 +4892,9 @@ class ModuleDependencyGraph {
3511
4892
  if (!this.fileOwners.has(normalized)) {
3512
4893
  this.fileOwners.set(normalized, new Set);
3513
4894
  }
3514
- this.fileOwners.get(normalized).add(moduleName);
4895
+ const owners = this.fileOwners.get(normalized);
4896
+ if (owners)
4897
+ owners.add(moduleName);
3515
4898
  }
3516
4899
  getModulesOwningFile(filePath) {
3517
4900
  const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
@@ -3527,12 +4910,14 @@ class ModuleDependencyGraph {
3527
4910
  }
3528
4911
  }
3529
4912
  if (directlyAffected.size === 0) {
3530
- return Array.from(this.moduleMap.keys());
4913
+ return [];
3531
4914
  }
3532
4915
  const affected = new Set(directlyAffected);
3533
4916
  const queue = Array.from(directlyAffected);
3534
4917
  while (queue.length > 0) {
3535
4918
  const current = queue.shift();
4919
+ if (!current)
4920
+ continue;
3536
4921
  const dependents = this.dependents.get(current);
3537
4922
  if (dependents) {
3538
4923
  for (const dep of dependents) {
@@ -3560,8 +4945,8 @@ function findAffectedModules(previous, current, changedFiles) {
3560
4945
  // src/watch.ts
3561
4946
  var DEFAULT_DEBOUNCE_MS = 100;
3562
4947
  function watchProject(options) {
3563
- const rootDir = resolve2(options.rootDir);
3564
- const outDir = resolve2(options.outDir);
4948
+ const rootDir = resolve4(options.rootDir);
4949
+ const outDir = resolve4(options.outDir);
3565
4950
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3566
4951
  let timer;
3567
4952
  let closed = false;
@@ -3571,8 +4956,12 @@ function watchProject(options) {
3571
4956
  let watcher;
3572
4957
  const incremental = createIncrementalCompiler();
3573
4958
  let initialEvent;
3574
- let resolveReady;
3575
- let rejectReady;
4959
+ let resolveReady = () => {
4960
+ return;
4961
+ };
4962
+ let rejectReady = () => {
4963
+ return;
4964
+ };
3576
4965
  const ready = new Promise((resolvePromise, rejectPromise) => {
3577
4966
  resolveReady = resolvePromise;
3578
4967
  rejectReady = rejectPromise;
@@ -3641,12 +5030,12 @@ function watchProject(options) {
3641
5030
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
3642
5031
  if (!filename)
3643
5032
  return schedule();
3644
- const changedPath = resolve2(rootDir, filename.toString());
3645
- const relativePath = relative3(outDir, changedPath);
5033
+ const changedPath = resolve4(rootDir, filename.toString());
5034
+ const relativePath = relative4(outDir, changedPath);
3646
5035
  if (!relativePath.startsWith("..") && relativePath !== "")
3647
5036
  return;
3648
5037
  if (/\.(tsx?|mts|cts)$/.test(changedPath))
3649
- schedule(relative3(rootDir, changedPath));
5038
+ schedule(relative4(rootDir, changedPath));
3650
5039
  });
3651
5040
  if (initialEvent)
3652
5041
  resolveReady(initialEvent);
@@ -3668,6 +5057,9 @@ function watchProject(options) {
3668
5057
  }
3669
5058
 
3670
5059
  // src/cli.ts
5060
+ function isModuleBoundaryPresetName(value) {
5061
+ return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
5062
+ }
3671
5063
  function printUsage() {
3672
5064
  console.log(`
3673
5065
  @supacloud/compiler CLI
@@ -3691,7 +5083,7 @@ Commands:
3691
5083
  Options:
3692
5084
  --root, -r <dir> Application source root (default: current directory or first positional argument)
3693
5085
  --out, -o <dir> Artifact output directory (default: <rootDir>/generated)
3694
- --strict Treat all warnings as errors
5086
+ --strict Enable type-safety gates and treat all warnings as errors
3695
5087
  --client Generate typed API client in client.ts
3696
5088
  --permissions Generate typed permissions registry in permissions.ts
3697
5089
  --debounce <ms> Debounce source changes in dev mode (default: 100)
@@ -3742,7 +5134,12 @@ async function run() {
3742
5134
  } else if (arg === "--json") {
3743
5135
  json = true;
3744
5136
  } else if (arg === "--preset" || arg === "-p") {
3745
- preset = args[++i];
5137
+ const presetArg = args[++i];
5138
+ if (!isModuleBoundaryPresetName(presetArg)) {
5139
+ console.error(`Error: --preset requires a known preset, received "${presetArg ?? ""}"`);
5140
+ process.exit(1);
5141
+ }
5142
+ preset = presetArg;
3746
5143
  } else if (!arg.startsWith("-") && rootDir === ".") {
3747
5144
  if (command === "explain" && !query)
3748
5145
  query = arg;
@@ -3752,8 +5149,8 @@ async function run() {
3752
5149
  query = arg;
3753
5150
  }
3754
5151
  }
3755
- const resolvedRoot = resolve3(process.cwd(), rootDir);
3756
- const resolvedOut = outDir ? resolve3(process.cwd(), outDir) : resolve3(resolvedRoot, "generated");
5152
+ const resolvedRoot = resolve5(process.cwd(), rootDir);
5153
+ const resolvedOut = outDir ? resolve5(process.cwd(), outDir) : resolve5(resolvedRoot, "generated");
3757
5154
  if (command === "compile") {
3758
5155
  const result = await compileProject({
3759
5156
  rootDir: resolvedRoot,