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