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