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