@supacloud/compiler 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/analyze.d.ts +2 -2
- package/dist/cli.js +1958 -561
- package/dist/generate.d.ts +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1950 -556
- 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 +73 -3
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,12 +1,383 @@
|
|
|
1
1
|
// src/analyze.ts
|
|
2
|
-
import {
|
|
2
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3
|
+
import { relative, resolve as resolvePath, sep } from "node:path";
|
|
4
|
+
import * as ts3 from "@typescript/typescript6";
|
|
5
|
+
|
|
6
|
+
// src/program.ts
|
|
7
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import * as ts2 from "@typescript/typescript6";
|
|
11
|
+
|
|
12
|
+
// src/traits.ts
|
|
3
13
|
import { createHash } from "node:crypto";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
14
|
+
import * as ts from "@typescript/typescript6";
|
|
15
|
+
class TraitCompiler {
|
|
16
|
+
handlers;
|
|
17
|
+
constructor(handlers = createDefaultTraitHandlers()) {
|
|
18
|
+
this.handlers = handlers;
|
|
19
|
+
}
|
|
20
|
+
compile(program, previous, changedFiles) {
|
|
21
|
+
const byFile = new Map;
|
|
22
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
23
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("/node_modules/"))
|
|
24
|
+
continue;
|
|
25
|
+
const previousTraits = previous?.byFile.get(sourceFile.fileName);
|
|
26
|
+
if (previousTraits && !changedFiles.has(sourceFile.fileName)) {
|
|
27
|
+
byFile.set(sourceFile.fileName, previousTraits);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
byFile.set(sourceFile.fileName, this.compileSourceFile(sourceFile));
|
|
31
|
+
}
|
|
32
|
+
const all = [...byFile.values()].flat().sort((a, b) => a.file.localeCompare(b.file) || a.start - b.start || a.kind.localeCompare(b.kind));
|
|
33
|
+
return { byFile, all };
|
|
34
|
+
}
|
|
35
|
+
compileSourceFile(sourceFile) {
|
|
36
|
+
const traits = [];
|
|
37
|
+
const visit = (node) => {
|
|
38
|
+
for (const handler of this.handlers) {
|
|
39
|
+
const name = handler.detect(node);
|
|
40
|
+
if (name)
|
|
41
|
+
traits.push(record(handler.kind, name, sourceFile, node));
|
|
42
|
+
}
|
|
43
|
+
ts.forEachChild(node, visit);
|
|
44
|
+
};
|
|
45
|
+
visit(sourceFile);
|
|
46
|
+
return traits;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function compileTraits(program, previous, changedFiles) {
|
|
50
|
+
return new TraitCompiler().compile(program, previous, changedFiles);
|
|
51
|
+
}
|
|
52
|
+
function record(kind, name, sourceFile, node) {
|
|
53
|
+
const text = node.getText(sourceFile);
|
|
54
|
+
return {
|
|
55
|
+
kind,
|
|
56
|
+
name,
|
|
57
|
+
file: sourceFile.fileName,
|
|
58
|
+
start: node.getStart(sourceFile),
|
|
59
|
+
end: node.end,
|
|
60
|
+
fingerprint: createHash("sha1").update(`${kind}:${text}`).digest("hex")
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function decoratorName(decorator) {
|
|
64
|
+
return expressionName(ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression);
|
|
65
|
+
}
|
|
66
|
+
function expressionName(expression) {
|
|
67
|
+
if (ts.isIdentifier(expression))
|
|
68
|
+
return expression.text;
|
|
69
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
70
|
+
return expression.name.text;
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
function createDefaultTraitHandlers() {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
kind: "module",
|
|
77
|
+
detect: decoratedDeclaration("Module")
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: "injectable",
|
|
81
|
+
detect: decoratedDeclaration("Injectable")
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
kind: "controller",
|
|
85
|
+
detect: decoratedDeclaration("Controller")
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
kind: "command",
|
|
89
|
+
detect: decoratedDeclaration("Command")
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
kind: "query",
|
|
93
|
+
detect: decoratedDeclaration("Query")
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
kind: "defineModule",
|
|
97
|
+
detect: (node) => {
|
|
98
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
|
|
99
|
+
return;
|
|
100
|
+
const initializer = node.initializer;
|
|
101
|
+
return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineModule" ? node.name.text : undefined;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
kind: "injectionToken",
|
|
106
|
+
detect: (node) => {
|
|
107
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
|
|
108
|
+
return;
|
|
109
|
+
const initializer = node.initializer;
|
|
110
|
+
return initializer && ts.isNewExpression(initializer) && expressionName(initializer.expression) === "InjectionToken" ? node.name.text : undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function decoratedDeclaration(decorator) {
|
|
116
|
+
return (node) => {
|
|
117
|
+
if (!ts.isClassDeclaration(node) || !node.name)
|
|
118
|
+
return;
|
|
119
|
+
return (ts.getDecorators(node) ?? []).some((item) => decoratorName(item) === decorator) ? node.name.text : undefined;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/program.ts
|
|
124
|
+
function createIncrementalProgramSession(projectRoot) {
|
|
125
|
+
const rootDir = resolve(projectRoot);
|
|
126
|
+
let projectConfig = readProjectConfig(rootDir);
|
|
127
|
+
let projectConfigKey = configKey(projectConfig);
|
|
128
|
+
let builder;
|
|
129
|
+
let traits;
|
|
130
|
+
const sourceFileCache = new Map;
|
|
131
|
+
return {
|
|
132
|
+
getProgram() {
|
|
133
|
+
if (!builder) {
|
|
134
|
+
throw new Error("incremental TypeScript program has not been initialized");
|
|
135
|
+
}
|
|
136
|
+
return builder.getProgram();
|
|
137
|
+
},
|
|
138
|
+
getTypeChecker() {
|
|
139
|
+
return this.getProgram().getTypeChecker();
|
|
140
|
+
},
|
|
141
|
+
update(rootNames, changedPaths = rootNames) {
|
|
142
|
+
const oldProgram = builder?.getProgram();
|
|
143
|
+
const oldSourceFiles = new Map(oldProgram?.getSourceFiles().map((sourceFile) => [canonical(sourceFile.fileName), sourceFile]) ?? []);
|
|
144
|
+
const nextProjectConfig = readProjectConfig(rootDir);
|
|
145
|
+
const nextProjectConfigKey = configKey(nextProjectConfig);
|
|
146
|
+
const configChanged = nextProjectConfigKey !== projectConfigKey;
|
|
147
|
+
const previousBuilder = configChanged ? undefined : builder;
|
|
148
|
+
if (configChanged) {
|
|
149
|
+
sourceFileCache.clear();
|
|
150
|
+
traits = undefined;
|
|
151
|
+
}
|
|
152
|
+
projectConfig = nextProjectConfig;
|
|
153
|
+
projectConfigKey = nextProjectConfigKey;
|
|
154
|
+
const normalizedRoots = [...new Set(rootNames.map((file) => resolve(rootDir, file)))].sort();
|
|
155
|
+
const normalizedChanged = [...new Set(changedPaths.map((file) => resolve(rootDir, file)))];
|
|
156
|
+
const invalidatedPaths = new Set(normalizedChanged.map(canonical));
|
|
157
|
+
for (const sourceFile of oldSourceFiles.values()) {
|
|
158
|
+
if (sourceVersion(sourceFile.fileName) !== sourceFileVersion(sourceFile)) {
|
|
159
|
+
invalidatedPaths.add(canonical(sourceFile.fileName));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const invalidateAllResolutions = configChanged || [...invalidatedPaths].some((fileName) => {
|
|
163
|
+
const wasInProgram = oldSourceFiles.has(fileName);
|
|
164
|
+
return wasInProgram !== existsSync(fileName);
|
|
165
|
+
});
|
|
166
|
+
for (const fileName of normalizedChanged) {
|
|
167
|
+
if (!existsSync(fileName))
|
|
168
|
+
sourceFileCache.delete(canonical(fileName));
|
|
169
|
+
}
|
|
170
|
+
const host = createHost(projectConfig.options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions);
|
|
171
|
+
builder = ts2.createEmitAndSemanticDiagnosticsBuilderProgram(normalizedRoots, projectConfig.options, host, previousBuilder, projectConfig.errors, projectConfig.projectReferences);
|
|
172
|
+
const program = builder.getProgram();
|
|
173
|
+
const changedFiles = [];
|
|
174
|
+
const reusedFiles = [];
|
|
175
|
+
const currentPaths = new Set(program.getSourceFiles().map((file) => canonical(file.fileName)));
|
|
176
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
177
|
+
if (sourceFile.isDeclarationFile)
|
|
178
|
+
continue;
|
|
179
|
+
const previous = oldSourceFiles.get(canonical(sourceFile.fileName));
|
|
180
|
+
if (previous && previous === sourceFile) {
|
|
181
|
+
reusedFiles.push(sourceFile.fileName);
|
|
182
|
+
} else {
|
|
183
|
+
changedFiles.push(sourceFile.fileName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const [path, sourceFile] of oldSourceFiles) {
|
|
187
|
+
if (!sourceFile.isDeclarationFile && !currentPaths.has(path)) {
|
|
188
|
+
changedFiles.push(sourceFile.fileName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const path of sourceFileCache.keys()) {
|
|
192
|
+
if (!currentPaths.has(path))
|
|
193
|
+
sourceFileCache.delete(path);
|
|
194
|
+
}
|
|
195
|
+
traits = compileTraits(program, traits, new Set(changedFiles));
|
|
196
|
+
return { changedFiles, reusedFiles, program };
|
|
197
|
+
},
|
|
198
|
+
getTraits() {
|
|
199
|
+
return traits?.all ?? [];
|
|
200
|
+
},
|
|
201
|
+
getDiagnostics() {
|
|
202
|
+
if (!builder)
|
|
203
|
+
return projectConfig.errors;
|
|
204
|
+
const program = builder.getProgram();
|
|
205
|
+
return [
|
|
206
|
+
...projectConfig.errors,
|
|
207
|
+
...program.getSyntacticDiagnostics()
|
|
208
|
+
];
|
|
209
|
+
},
|
|
210
|
+
emit() {
|
|
211
|
+
if (!builder) {
|
|
212
|
+
throw new Error("incremental TypeScript program has not been initialized");
|
|
213
|
+
}
|
|
214
|
+
return builder.emit();
|
|
215
|
+
},
|
|
216
|
+
reset() {
|
|
217
|
+
builder = undefined;
|
|
218
|
+
projectConfig = readProjectConfig(rootDir);
|
|
219
|
+
projectConfigKey = configKey(projectConfig);
|
|
220
|
+
traits = undefined;
|
|
221
|
+
sourceFileCache.clear();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function createHost(options, rootDir, sourceFileCache, invalidatedPaths, invalidateAllResolutions) {
|
|
226
|
+
const host = ts2.createIncrementalCompilerHost(options, {
|
|
227
|
+
...ts2.sys,
|
|
228
|
+
getCurrentDirectory: () => rootDir
|
|
229
|
+
});
|
|
230
|
+
host.hasInvalidatedResolutions = (filePath) => invalidateAllResolutions || invalidatedPaths.has(canonical(filePath));
|
|
231
|
+
const originalGetSourceFile = host.getSourceFile.bind(host);
|
|
232
|
+
host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
|
|
233
|
+
const key = canonical(fileName);
|
|
234
|
+
const text = host.readFile(fileName);
|
|
235
|
+
if (text === undefined) {
|
|
236
|
+
sourceFileCache.delete(key);
|
|
237
|
+
return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
238
|
+
}
|
|
239
|
+
const version = hashText(text);
|
|
240
|
+
const parseKey = sourceFileParseKey(languageVersion);
|
|
241
|
+
const cached = sourceFileCache.get(key);
|
|
242
|
+
if (!shouldCreateNewSourceFile && cached?.version === version && cached.parseKey === parseKey) {
|
|
243
|
+
return cached.sourceFile;
|
|
244
|
+
}
|
|
245
|
+
const sourceFile = originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
246
|
+
if (sourceFile) {
|
|
247
|
+
sourceFileCache.set(key, { sourceFile, version: hashText(sourceFile.text), parseKey });
|
|
248
|
+
} else {
|
|
249
|
+
sourceFileCache.delete(key);
|
|
250
|
+
}
|
|
251
|
+
return sourceFile;
|
|
252
|
+
};
|
|
253
|
+
return host;
|
|
254
|
+
}
|
|
255
|
+
function canonical(fileName) {
|
|
256
|
+
const normalized = resolve(fileName);
|
|
257
|
+
return ts2.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
function sourceFileParseKey(languageVersion) {
|
|
260
|
+
return typeof languageVersion === "number" ? `target:${languageVersion}` : JSON.stringify({
|
|
261
|
+
languageVersion: languageVersion.languageVersion,
|
|
262
|
+
impliedNodeFormat: languageVersion.impliedNodeFormat,
|
|
263
|
+
jsDocParsingMode: languageVersion.jsDocParsingMode
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function configKey(config) {
|
|
267
|
+
return JSON.stringify({
|
|
268
|
+
options: config.options,
|
|
269
|
+
projectReferences: config.projectReferences,
|
|
270
|
+
configFingerprint: config.configFingerprint
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function hashText(text) {
|
|
274
|
+
return createHash2("sha1").update(text).digest("hex");
|
|
275
|
+
}
|
|
276
|
+
function sourceVersion(fileName) {
|
|
277
|
+
try {
|
|
278
|
+
return hashText(readFileSync(fileName, "utf8"));
|
|
279
|
+
} catch {
|
|
280
|
+
return "missing";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function sourceFileVersion(sourceFile) {
|
|
284
|
+
const descriptor = Object.getOwnPropertyDescriptor(sourceFile, "version");
|
|
285
|
+
return typeof descriptor?.value === "string" ? descriptor.value : undefined;
|
|
286
|
+
}
|
|
287
|
+
function readProjectConfig(rootDir) {
|
|
288
|
+
const configPath = join(rootDir, "tsconfig.json");
|
|
289
|
+
if (!existsSync(configPath)) {
|
|
290
|
+
return {
|
|
291
|
+
options: {
|
|
292
|
+
target: ts2.ScriptTarget.ES2022,
|
|
293
|
+
module: ts2.ModuleKind.ESNext,
|
|
294
|
+
moduleResolution: ts2.ModuleResolutionKind.Bundler,
|
|
295
|
+
experimentalDecorators: true,
|
|
296
|
+
allowJs: false,
|
|
297
|
+
skipLibCheck: true
|
|
298
|
+
},
|
|
299
|
+
errors: [],
|
|
300
|
+
projectReferences: undefined,
|
|
301
|
+
configFingerprint: "defaults"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const configReads = new Map;
|
|
305
|
+
const readConfig = (fileName) => {
|
|
306
|
+
const text = ts2.sys.readFile(fileName);
|
|
307
|
+
configReads.set(canonical(fileName), text === undefined ? "missing" : hashText(text));
|
|
308
|
+
return text;
|
|
309
|
+
};
|
|
310
|
+
const config = ts2.readConfigFile(configPath, readConfig);
|
|
311
|
+
if (config.error) {
|
|
312
|
+
return {
|
|
313
|
+
options: {},
|
|
314
|
+
errors: [config.error],
|
|
315
|
+
configFingerprint: JSON.stringify([...configReads])
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const parsed = ts2.parseJsonConfigFileContent(config.config, { ...ts2.sys, readFile: readConfig }, dirname(configPath));
|
|
319
|
+
return {
|
|
320
|
+
options: parsed.options,
|
|
321
|
+
errors: parsed.errors,
|
|
322
|
+
projectReferences: parsed.projectReferences,
|
|
323
|
+
configFingerprint: JSON.stringify([...configReads])
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/util.ts
|
|
328
|
+
function camelName(token) {
|
|
329
|
+
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
330
|
+
if (isConstantCase) {
|
|
331
|
+
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
332
|
+
}
|
|
333
|
+
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
334
|
+
}
|
|
335
|
+
function relativeImportPath(fromDir, toFile) {
|
|
336
|
+
const fromParts = fromDir.split("/").filter(Boolean);
|
|
337
|
+
const toParts = toFile.split("/").filter(Boolean);
|
|
338
|
+
let common = 0;
|
|
339
|
+
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
340
|
+
common += 1;
|
|
341
|
+
}
|
|
342
|
+
const ups = fromParts.length - common;
|
|
343
|
+
const downs = toParts.slice(common);
|
|
344
|
+
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
345
|
+
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
346
|
+
const joined = segments.join("/");
|
|
347
|
+
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
348
|
+
}
|
|
349
|
+
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
350
|
+
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
351
|
+
function isRequestContextToken(token, tokenNames) {
|
|
352
|
+
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
353
|
+
}
|
|
354
|
+
function isJobContextToken(token, tokenNames) {
|
|
355
|
+
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
356
|
+
}
|
|
357
|
+
function joinRoutePaths(prefix, path) {
|
|
358
|
+
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
359
|
+
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
360
|
+
return normalized;
|
|
361
|
+
}
|
|
362
|
+
function findClosestMatch(target, candidates) {
|
|
363
|
+
if (candidates.length === 0)
|
|
364
|
+
return;
|
|
365
|
+
if (candidates.length === 1)
|
|
366
|
+
return candidates[0];
|
|
367
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
368
|
+
const targetNorm = norm(target);
|
|
369
|
+
for (const c of candidates) {
|
|
370
|
+
if (norm(c) === targetNorm)
|
|
371
|
+
return c;
|
|
372
|
+
}
|
|
373
|
+
for (const c of candidates) {
|
|
374
|
+
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
375
|
+
return c;
|
|
376
|
+
}
|
|
377
|
+
return candidates[0];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/analyze.ts
|
|
10
381
|
var DEFAULT_INCLUDE = ["**/*.module.ts", "**/*.ts"];
|
|
11
382
|
var ROUTE_DECORATORS = {
|
|
12
383
|
Get: "GET",
|
|
@@ -18,63 +389,118 @@ var ROUTE_DECORATORS = {
|
|
|
18
389
|
Options: "OPTIONS"
|
|
19
390
|
};
|
|
20
391
|
var SCOPES = ["application", "request", "job"];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
392
|
+
function isScope(value) {
|
|
393
|
+
return SCOPES.some((scope) => scope === value);
|
|
394
|
+
}
|
|
395
|
+
function nodeText(node) {
|
|
396
|
+
return node.getText(node.getSourceFile());
|
|
397
|
+
}
|
|
398
|
+
function lineOf(node) {
|
|
399
|
+
const sourceFile = node.getSourceFile();
|
|
400
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
401
|
+
}
|
|
402
|
+
function variableName(decl) {
|
|
403
|
+
return ts3.isIdentifier(decl.name) ? decl.name.text : nodeText(decl.name);
|
|
404
|
+
}
|
|
405
|
+
function propertyName(name) {
|
|
406
|
+
if (ts3.isIdentifier(name) || ts3.isPrivateIdentifier(name))
|
|
407
|
+
return name.text;
|
|
408
|
+
if (ts3.isStringLiteral(name) || ts3.isNumericLiteral(name))
|
|
409
|
+
return name.text;
|
|
410
|
+
return nodeText(name);
|
|
411
|
+
}
|
|
412
|
+
function parameterName(param) {
|
|
413
|
+
return ts3.isIdentifier(param.name) ? param.name.text : nodeText(param.name);
|
|
414
|
+
}
|
|
415
|
+
function decoratorsOf(node) {
|
|
416
|
+
return ts3.canHaveDecorators(node) ? ts3.getDecorators(node) ?? [] : [];
|
|
417
|
+
}
|
|
418
|
+
function decoratorArguments(dec) {
|
|
419
|
+
return ts3.isCallExpression(dec.expression) ? dec.expression.arguments : [];
|
|
420
|
+
}
|
|
421
|
+
function hasMethod(cls, name) {
|
|
422
|
+
return cls.members.some((member) => (ts3.isMethodDeclaration(member) || ts3.isGetAccessorDeclaration(member) || ts3.isSetAccessorDeclaration(member)) && member.name !== undefined && propertyName(member.name) === name);
|
|
423
|
+
}
|
|
424
|
+
function hasDestroyHook(cls) {
|
|
425
|
+
return hasMethod(cls, "onDestroy") || hasMethod(cls, "ngOnDestroy");
|
|
426
|
+
}
|
|
427
|
+
function descendantsOfKind(root, predicate) {
|
|
428
|
+
const result = [];
|
|
429
|
+
const visit = (node) => {
|
|
430
|
+
if (predicate(node))
|
|
431
|
+
result.push(node);
|
|
432
|
+
ts3.forEachChild(node, visit);
|
|
433
|
+
};
|
|
434
|
+
visit(root);
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
438
|
+
const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
|
|
439
|
+
if (cache)
|
|
440
|
+
cache.programSession = session;
|
|
441
|
+
const rootNames = ts3.sys.readDirectory(rootDir, [".ts", ".tsx"], ["node_modules", "dist"], include ?? DEFAULT_INCLUDE);
|
|
442
|
+
const update = session.update(rootNames, changedPaths);
|
|
443
|
+
const program = update.program;
|
|
444
|
+
const checker = program.getTypeChecker();
|
|
445
|
+
const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("/node_modules/") && !sf.fileName.includes("/dist/") && isProjectSourceFile(sf, rootDir)).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
36
446
|
const ctx = {
|
|
37
447
|
rootDir,
|
|
448
|
+
program,
|
|
449
|
+
checker,
|
|
38
450
|
tokensByName: new Map,
|
|
39
451
|
classesByName: new Map,
|
|
40
452
|
diagnostics: []
|
|
41
453
|
};
|
|
454
|
+
const nativeTraitFiles = new Map;
|
|
455
|
+
for (const diagnostic of session.getDiagnostics()) {
|
|
456
|
+
ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
|
|
457
|
+
}
|
|
458
|
+
for (const trait of session.getTraits()) {
|
|
459
|
+
const kinds = nativeTraitFiles.get(trait.file) ?? new Set;
|
|
460
|
+
kinds.add(trait.kind);
|
|
461
|
+
nativeTraitFiles.set(trait.file, kinds);
|
|
462
|
+
}
|
|
42
463
|
for (const sf of sourceFiles) {
|
|
43
464
|
indexFile(sf, ctx);
|
|
44
465
|
}
|
|
45
466
|
const candidates = [];
|
|
46
467
|
for (const sf of sourceFiles) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
468
|
+
const traits = nativeTraitFiles.get(sf.fileName);
|
|
469
|
+
if (!cache || traits?.has("module")) {
|
|
470
|
+
for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
|
|
471
|
+
const moduleDec = findDecorator(cls, "Module");
|
|
472
|
+
if (!moduleDec)
|
|
473
|
+
continue;
|
|
474
|
+
const options = decoratorObjectArg(moduleDec);
|
|
475
|
+
if (!options)
|
|
476
|
+
continue;
|
|
477
|
+
candidates.push({
|
|
478
|
+
node: cls,
|
|
479
|
+
options,
|
|
480
|
+
className: cls.name?.text ?? "<anonymous>",
|
|
481
|
+
file: sf.fileName,
|
|
482
|
+
line: lineOf(cls)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
61
485
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
486
|
+
if (!cache || traits?.has("defineModule")) {
|
|
487
|
+
for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
|
|
488
|
+
if (nodeText(call.expression) !== "defineModule")
|
|
489
|
+
continue;
|
|
490
|
+
const parent = call.parent;
|
|
491
|
+
if (!parent || !ts3.isVariableDeclaration(parent))
|
|
492
|
+
continue;
|
|
493
|
+
const arg = call.arguments[0];
|
|
494
|
+
if (!arg || !ts3.isObjectLiteralExpression(arg))
|
|
495
|
+
continue;
|
|
496
|
+
candidates.push({
|
|
497
|
+
node: parent,
|
|
498
|
+
options: arg,
|
|
499
|
+
className: variableName(parent),
|
|
500
|
+
file: sf.fileName,
|
|
501
|
+
line: lineOf(parent)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
78
504
|
}
|
|
79
505
|
}
|
|
80
506
|
const nameByNode = new Map;
|
|
@@ -87,8 +513,8 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
87
513
|
if (cache) {
|
|
88
514
|
const currentFileHashes = new Map;
|
|
89
515
|
for (const sf of sourceFiles) {
|
|
90
|
-
const rel = sourcePath(rootDir, sf.
|
|
91
|
-
const hash =
|
|
516
|
+
const rel = sourcePath(rootDir, sf.fileName);
|
|
517
|
+
const hash = createHash3("sha256").update(sf.getFullText()).digest("hex");
|
|
92
518
|
currentFileHashes.set(rel, hash);
|
|
93
519
|
}
|
|
94
520
|
const changedFiles = new Set;
|
|
@@ -104,7 +530,7 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
104
530
|
}
|
|
105
531
|
const modulesToKeep = new Map;
|
|
106
532
|
const finalModules = [];
|
|
107
|
-
const finalDiagnostics = [];
|
|
533
|
+
const finalDiagnostics = [...ctx.diagnostics];
|
|
108
534
|
const affectedModuleNames = cache.dependencyGraph && typeof cache.dependencyGraph.getAffectedModules === "function" ? new Set(cache.dependencyGraph.getAffectedModules(Array.from(changedFiles))) : new Set;
|
|
109
535
|
for (const [modName, entry] of cache.modules.entries()) {
|
|
110
536
|
const hasChangedFile = entry.ownedFiles.some((f) => changedFiles.has(f));
|
|
@@ -126,14 +552,7 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
126
552
|
const diagBefore = ctx.diagnostics.length;
|
|
127
553
|
const parsed = parseModule(c, nameByNode, ctx);
|
|
128
554
|
const moduleDiagnostics = ctx.diagnostics.slice(diagBefore);
|
|
129
|
-
const ownedFiles =
|
|
130
|
-
ownedFiles.add(parsed.file);
|
|
131
|
-
for (const p of parsed.providers)
|
|
132
|
-
if (p.file)
|
|
133
|
-
ownedFiles.add(p.file);
|
|
134
|
-
for (const ctrl of parsed.controllers)
|
|
135
|
-
if (ctrl.file)
|
|
136
|
-
ownedFiles.add(ctrl.file);
|
|
555
|
+
const ownedFiles = collectModuleSourceClosure(parsed, ctx);
|
|
137
556
|
const fileHashes = {};
|
|
138
557
|
for (const f of ownedFiles) {
|
|
139
558
|
fileHashes[f] = currentFileHashes.get(f) ?? "";
|
|
@@ -160,6 +579,7 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
160
579
|
} else {
|
|
161
580
|
modules = candidates.map((c) => parseModule(c, nameByNode, ctx));
|
|
162
581
|
}
|
|
582
|
+
modules.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
163
583
|
const allRegisteredClasses = new Set;
|
|
164
584
|
const allRegisteredControllers = new Set;
|
|
165
585
|
const allRegisteredCommands = new Set;
|
|
@@ -184,9 +604,9 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
184
604
|
if (!allRegisteredClasses.has(name)) {
|
|
185
605
|
const injectable = parseInjectableOptions(classInfo.decl, ctx);
|
|
186
606
|
if (injectable?.providedIn === "root") {
|
|
187
|
-
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = classDeps(classInfo.decl, ctx);
|
|
607
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(classInfo.decl, ctx);
|
|
188
608
|
const file = sourcePath(ctx.rootDir, classInfo.file);
|
|
189
|
-
const line = classInfo.decl
|
|
609
|
+
const line = lineOf(classInfo.decl);
|
|
190
610
|
if (missing) {
|
|
191
611
|
warn(ctx, "missing-deps", `root provider ${name} 的部分构造依赖无法静态解析`, file, line);
|
|
192
612
|
}
|
|
@@ -201,8 +621,9 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
201
621
|
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
202
622
|
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
203
623
|
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
624
|
+
functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
|
|
204
625
|
providedIn: "root",
|
|
205
|
-
hasOnDestroy: classInfo.decl
|
|
626
|
+
hasOnDestroy: hasDestroyHook(classInfo.decl) || undefined,
|
|
206
627
|
exported: true,
|
|
207
628
|
file,
|
|
208
629
|
line,
|
|
@@ -213,8 +634,8 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
213
634
|
if (!allRegisteredControllers.has(name)) {
|
|
214
635
|
const controllerDec = findDecorator(classInfo.decl, "Controller");
|
|
215
636
|
if (controllerDec) {
|
|
216
|
-
const arg = controllerDec
|
|
217
|
-
const isStandalone = arg &&
|
|
637
|
+
const arg = decoratorArguments(controllerDec)[0];
|
|
638
|
+
const isStandalone = arg && ts3.isObjectLiteralExpression(arg) && booleanProp(arg, "standalone");
|
|
218
639
|
if (isStandalone) {
|
|
219
640
|
const ctrl = parseController(classInfo.decl, ctx);
|
|
220
641
|
if (ctrl)
|
|
@@ -228,13 +649,14 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
228
649
|
const meta = decoratorObjectArg(commandDec);
|
|
229
650
|
if (meta && booleanProp(meta, "standalone")) {
|
|
230
651
|
standaloneCommands.push({
|
|
231
|
-
className: classInfo.decl.
|
|
232
|
-
name: stringLiteralProp(meta, "name") ?? classInfo.decl.
|
|
652
|
+
className: classInfo.decl.name?.text ?? name,
|
|
653
|
+
name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
|
|
233
654
|
permission: stringLiteralProp(meta, "permission"),
|
|
234
655
|
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
235
656
|
audit: stringLiteralProp(meta, "audit"),
|
|
236
657
|
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
237
|
-
standalone: true
|
|
658
|
+
standalone: true,
|
|
659
|
+
aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
|
|
238
660
|
});
|
|
239
661
|
}
|
|
240
662
|
}
|
|
@@ -259,13 +681,13 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
259
681
|
if (rootProviders.length > 0 || standaloneControllers.length > 0 || standaloneCommands.length > 0) {
|
|
260
682
|
const existingRoot = modules.find((m) => m.name === "root" || m.name === "app");
|
|
261
683
|
if (existingRoot) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
684
|
+
modules = modules.map((module) => module === existingRoot ? {
|
|
685
|
+
...module,
|
|
686
|
+
providers: [...module.providers, ...rootProviders],
|
|
687
|
+
controllers: [...module.controllers, ...standaloneControllers],
|
|
688
|
+
commands: [...module.commands, ...standaloneCommands],
|
|
689
|
+
exports: [...new Set([...module.exports, ...rootProviders.map((provider) => provider.token)])]
|
|
690
|
+
} : module);
|
|
269
691
|
} else {
|
|
270
692
|
const fallbackFile = rootProviders[0]?.file ?? standaloneControllers[0]?.file ?? "root.ts";
|
|
271
693
|
modules.unshift({
|
|
@@ -304,25 +726,72 @@ async function analyzeProject(rootDir, include, cache) {
|
|
|
304
726
|
cacheStats: cache ? { reusedModules, reanalyzedModules } : undefined
|
|
305
727
|
};
|
|
306
728
|
}
|
|
307
|
-
function
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
729
|
+
function collectModuleSourceClosure(module, ctx) {
|
|
730
|
+
const seeds = new Set;
|
|
731
|
+
const addRelativeModule = (path) => {
|
|
732
|
+
if (!path)
|
|
733
|
+
return;
|
|
734
|
+
const withExtension = /\.(tsx?|mts|cts|js)$/.test(path) ? path : `${path}.ts`;
|
|
735
|
+
seeds.add(resolveSourcePath(ctx.rootDir, withExtension));
|
|
736
|
+
};
|
|
737
|
+
addRelativeModule(module.file);
|
|
738
|
+
for (const provider of module.providers)
|
|
739
|
+
addRelativeModule(provider.importPath);
|
|
740
|
+
for (const controller of module.controllers)
|
|
741
|
+
addRelativeModule(controller.importPath);
|
|
742
|
+
const ownedFiles = new Set;
|
|
743
|
+
const queue = [...seeds];
|
|
744
|
+
while (queue.length > 0) {
|
|
745
|
+
const fileName = queue.shift();
|
|
746
|
+
if (!fileName)
|
|
747
|
+
continue;
|
|
748
|
+
const sourceFile = ctx.program.getSourceFile(fileName);
|
|
749
|
+
if (!sourceFile || sourceFile.isDeclarationFile || !isProjectSourceFile(sourceFile, ctx.rootDir))
|
|
750
|
+
continue;
|
|
751
|
+
const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
|
|
752
|
+
if (ownedFiles.has(relativeFile))
|
|
753
|
+
continue;
|
|
754
|
+
ownedFiles.add(relativeFile);
|
|
755
|
+
for (const statement of sourceFile.statements) {
|
|
756
|
+
let moduleName;
|
|
757
|
+
if (ts3.isImportDeclaration(statement) && ts3.isStringLiteral(statement.moduleSpecifier)) {
|
|
758
|
+
moduleName = statement.moduleSpecifier.text;
|
|
759
|
+
} else if (ts3.isExportDeclaration(statement) && statement.moduleSpecifier && ts3.isStringLiteral(statement.moduleSpecifier)) {
|
|
760
|
+
moduleName = statement.moduleSpecifier.text;
|
|
761
|
+
} else if (ts3.isImportEqualsDeclaration(statement) && ts3.isExternalModuleReference(statement.moduleReference) && ts3.isStringLiteral(statement.moduleReference.expression)) {
|
|
762
|
+
moduleName = statement.moduleReference.expression.text;
|
|
763
|
+
}
|
|
764
|
+
if (!moduleName || moduleName.startsWith("node:"))
|
|
765
|
+
continue;
|
|
766
|
+
const resolved = ts3.resolveModuleName(moduleName, sourceFile.fileName, ctx.program.getCompilerOptions(), ts3.sys).resolvedModule?.resolvedFileName;
|
|
767
|
+
if (resolved && isProjectSourcePath(resolved, ctx.rootDir))
|
|
768
|
+
queue.push(resolved);
|
|
769
|
+
}
|
|
311
770
|
}
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
|
|
771
|
+
return ownedFiles;
|
|
772
|
+
}
|
|
773
|
+
function resolveSourcePath(rootDir, file) {
|
|
774
|
+
const normalized = file.replace(/\\/g, "/");
|
|
775
|
+
return resolvePath(rootDir, normalized);
|
|
776
|
+
}
|
|
777
|
+
function isProjectSourcePath(fileName, rootDir) {
|
|
778
|
+
const normalized = fileName.replace(/\\/g, "/");
|
|
779
|
+
const root = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
780
|
+
return normalized === root || normalized.startsWith(`${root}/`);
|
|
781
|
+
}
|
|
782
|
+
function isProjectSourceFile(sourceFile, rootDir) {
|
|
783
|
+
return isProjectSourcePath(sourceFile.fileName, rootDir) && /\.(tsx?|mts|cts)$/.test(sourceFile.fileName);
|
|
315
784
|
}
|
|
316
785
|
function indexFile(sf, ctx) {
|
|
317
|
-
for (const cls of sf.
|
|
318
|
-
const name = cls.
|
|
786
|
+
for (const cls of sf.statements.filter(ts3.isClassDeclaration)) {
|
|
787
|
+
const name = cls.name?.text;
|
|
319
788
|
if (name && !ctx.classesByName.has(name)) {
|
|
320
|
-
ctx.classesByName.set(name, { name, decl: cls, file: sf.
|
|
789
|
+
ctx.classesByName.set(name, { name, decl: cls, file: sf.fileName });
|
|
321
790
|
}
|
|
322
791
|
}
|
|
323
|
-
for (const statement of sf.
|
|
324
|
-
for (const decl of statement.
|
|
325
|
-
const info = parseTokenVariable(decl, sf.
|
|
792
|
+
for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
|
|
793
|
+
for (const decl of statement.declarationList.declarations) {
|
|
794
|
+
const info = parseTokenVariable(decl, sf.fileName);
|
|
326
795
|
if (info && !ctx.tokensByName.has(info.name)) {
|
|
327
796
|
ctx.tokensByName.set(info.name, info);
|
|
328
797
|
}
|
|
@@ -330,19 +799,19 @@ function indexFile(sf, ctx) {
|
|
|
330
799
|
}
|
|
331
800
|
}
|
|
332
801
|
function parseTokenVariable(decl, file) {
|
|
333
|
-
const init = decl.
|
|
334
|
-
if (!init || !
|
|
802
|
+
const init = decl.initializer;
|
|
803
|
+
if (!init || !ts3.isNewExpression(init))
|
|
335
804
|
return;
|
|
336
|
-
if (init.
|
|
805
|
+
if (nodeText(init.expression) !== "InjectionToken")
|
|
337
806
|
return;
|
|
338
|
-
const [nameArg, optionsArg] = init.
|
|
339
|
-
const info = { name: decl
|
|
340
|
-
if (nameArg &&
|
|
341
|
-
info.stringName = nameArg.
|
|
807
|
+
const [nameArg, optionsArg] = init.arguments ?? [];
|
|
808
|
+
const info = { name: variableName(decl), file, line: lineOf(decl) };
|
|
809
|
+
if (nameArg && ts3.isStringLiteral(nameArg)) {
|
|
810
|
+
info.stringName = nameArg.text;
|
|
342
811
|
}
|
|
343
|
-
if (optionsArg &&
|
|
812
|
+
if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
|
|
344
813
|
const scope = stringLiteralProp(optionsArg, "scope");
|
|
345
|
-
if (scope &&
|
|
814
|
+
if (scope && isScope(scope)) {
|
|
346
815
|
info.scope = scope;
|
|
347
816
|
}
|
|
348
817
|
const providedIn = stringLiteralProp(optionsArg, "providedIn");
|
|
@@ -359,33 +828,76 @@ function parseTokenVariable(decl, file) {
|
|
|
359
828
|
function parseModule(candidate, nameByNode, ctx) {
|
|
360
829
|
const { options, className, file, line } = candidate;
|
|
361
830
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
362
|
-
const tags = arrayProp(options, "tags").map((el) =>
|
|
831
|
+
const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
|
|
832
|
+
const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
|
|
363
833
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
364
834
|
const unwrapped = unwrapForwardRef(el);
|
|
365
|
-
const decl =
|
|
835
|
+
const decl = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
|
|
366
836
|
if (decl) {
|
|
367
837
|
const known = nameByNode.get(decl);
|
|
368
838
|
if (known)
|
|
369
839
|
return known;
|
|
370
|
-
if (
|
|
840
|
+
if (ts3.isClassDeclaration(decl)) {
|
|
371
841
|
const dec = findDecorator(decl, "Module");
|
|
372
842
|
const decOptions = dec && decoratorObjectArg(dec);
|
|
373
843
|
const decName = decOptions && stringLiteralProp(decOptions, "name");
|
|
374
|
-
return decName ?? decl.
|
|
844
|
+
return decName ?? decl.name?.text ?? nodeText(el);
|
|
375
845
|
}
|
|
376
|
-
if (
|
|
377
|
-
return decl
|
|
846
|
+
if (ts3.isVariableDeclaration(decl))
|
|
847
|
+
return variableName(decl);
|
|
378
848
|
}
|
|
379
|
-
return el
|
|
849
|
+
return nodeText(el);
|
|
380
850
|
}).filter((v, i, arr) => arr.indexOf(v) === i);
|
|
381
851
|
const exports = arrayProp(options, "exports").map((el) => tokenNameOf(el, ctx).name);
|
|
382
852
|
const exportsSet = new Set(exports);
|
|
383
853
|
const providers = [];
|
|
384
|
-
for (const el of arrayProp(options, "providers")) {
|
|
854
|
+
for (const el of expandProviderExpressions(arrayProp(options, "providers"), ctx)) {
|
|
855
|
+
const parsedProviders = parseFunctionalProvider(el, exportsSet, ctx);
|
|
856
|
+
if (parsedProviders) {
|
|
857
|
+
providers.push(...parsedProviders);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (ts3.isCallExpression(el)) {
|
|
861
|
+
const helper = nodeText(el.expression).split(".").pop() ?? nodeText(el.expression);
|
|
862
|
+
warn(ctx, "unsupported-provider-helper", `无法静态展开 provider helper '${helper}';请改用显式 Provider 或实现编译器支持的 helper`, sourcePath(ctx.rootDir, el.getSourceFile().fileName), lineOf(el));
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
385
865
|
const provider = parseProvider(el, exportsSet, ctx);
|
|
386
866
|
if (provider)
|
|
387
867
|
providers.push(provider);
|
|
388
868
|
}
|
|
869
|
+
for (const el of arrayProp(options, "jobs")) {
|
|
870
|
+
if (!ts3.isIdentifier(el))
|
|
871
|
+
continue;
|
|
872
|
+
const decl = resolveDeclaration(el, ctx)[0];
|
|
873
|
+
if (!decl || !ts3.isClassDeclaration(decl))
|
|
874
|
+
continue;
|
|
875
|
+
const className2 = decl.name?.text ?? el.text;
|
|
876
|
+
const registeredProvider = providers.find((provider) => provider.token === className2 || provider.useClass === className2);
|
|
877
|
+
if (registeredProvider)
|
|
878
|
+
continue;
|
|
879
|
+
const deps = classDeps(decl, ctx);
|
|
880
|
+
const injectable = parseInjectableOptions(decl, ctx);
|
|
881
|
+
const scope = injectable?.scope ?? "job";
|
|
882
|
+
providers.push({
|
|
883
|
+
token: className2,
|
|
884
|
+
tokenKind: "class",
|
|
885
|
+
kind: "class",
|
|
886
|
+
useClass: className2,
|
|
887
|
+
scope,
|
|
888
|
+
deps: deps.deps,
|
|
889
|
+
optionalDeps: deps.optionalDeps.length > 0 ? deps.optionalDeps : undefined,
|
|
890
|
+
selfDeps: deps.selfDeps.length > 0 ? deps.selfDeps : undefined,
|
|
891
|
+
skipSelfDeps: deps.skipSelfDeps.length > 0 ? deps.skipSelfDeps : undefined,
|
|
892
|
+
hostDeps: deps.hostDeps.length > 0 ? deps.hostDeps : undefined,
|
|
893
|
+
functionalInjects: deps.functionalInjects.length > 0 ? deps.functionalInjects : undefined,
|
|
894
|
+
hasOnDestroy: hasDestroyHook(decl) || undefined,
|
|
895
|
+
exported: exportsSet.has(className2),
|
|
896
|
+
file: sourcePath(ctx.rootDir, decl.getSourceFile().fileName),
|
|
897
|
+
line: lineOf(decl),
|
|
898
|
+
importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName)
|
|
899
|
+
});
|
|
900
|
+
}
|
|
389
901
|
const controllers = [];
|
|
390
902
|
for (const el of arrayProp(options, "controllers")) {
|
|
391
903
|
const controller = parseController(el, ctx);
|
|
@@ -395,40 +907,74 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
395
907
|
const handlerClasses = [];
|
|
396
908
|
const seenHandlers = new Set;
|
|
397
909
|
const collectHandler = (expr) => {
|
|
398
|
-
if (!
|
|
910
|
+
if (!ts3.isIdentifier(expr))
|
|
399
911
|
return;
|
|
400
|
-
const decl = resolveDeclaration(expr)[0];
|
|
401
|
-
if (decl &&
|
|
402
|
-
seenHandlers.add(decl.
|
|
912
|
+
const decl = resolveDeclaration(expr, ctx)[0];
|
|
913
|
+
if (decl && ts3.isClassDeclaration(decl) && !seenHandlers.has(decl.name?.text ?? "")) {
|
|
914
|
+
seenHandlers.add(decl.name?.text ?? "");
|
|
403
915
|
handlerClasses.push(decl);
|
|
404
916
|
}
|
|
405
917
|
};
|
|
406
918
|
for (const el of arrayProp(options, "providers")) {
|
|
407
|
-
if (
|
|
919
|
+
if (ts3.isIdentifier(el))
|
|
408
920
|
collectHandler(el);
|
|
409
|
-
if (
|
|
921
|
+
if (ts3.isObjectLiteralExpression(el)) {
|
|
410
922
|
const useClass = getProp(el, "useClass");
|
|
411
923
|
if (useClass)
|
|
412
924
|
collectHandler(useClass);
|
|
413
925
|
}
|
|
414
926
|
}
|
|
415
927
|
arrayProp(options, "commands").forEach(collectHandler);
|
|
928
|
+
arrayProp(options, "jobs").forEach(collectHandler);
|
|
416
929
|
arrayProp(options, "queries").forEach(collectHandler);
|
|
417
930
|
const commands = [];
|
|
931
|
+
const jobs = [];
|
|
418
932
|
const queries = [];
|
|
419
933
|
for (const cls of handlerClasses) {
|
|
420
934
|
const commandDec = findDecorator(cls, "Command");
|
|
421
935
|
if (commandDec) {
|
|
422
936
|
const meta = decoratorObjectArg(commandDec);
|
|
423
937
|
if (meta) {
|
|
938
|
+
const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
424
939
|
commands.push({
|
|
425
|
-
className: cls.
|
|
426
|
-
name: stringLiteralProp(meta, "name") ?? cls.
|
|
940
|
+
className: cls.name?.text ?? "<anonymous>",
|
|
941
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
427
942
|
permission: stringLiteralProp(meta, "permission"),
|
|
428
943
|
transaction: commandModeProp(meta, "transaction") ?? "none",
|
|
429
944
|
audit: stringLiteralProp(meta, "audit"),
|
|
430
945
|
idempotency: commandModeProp(meta, "idempotency") ?? "none",
|
|
431
|
-
|
|
946
|
+
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
947
|
+
...aspects2.length > 0 ? { aspects: aspects2 } : {}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const jobDec = findDecorator(cls, "Job");
|
|
952
|
+
if (jobDec) {
|
|
953
|
+
const meta = decoratorObjectArg(jobDec);
|
|
954
|
+
if (meta) {
|
|
955
|
+
const injectable = parseInjectableOptions(cls, ctx);
|
|
956
|
+
const className2 = cls.name?.text ?? "<anonymous>";
|
|
957
|
+
const provider = providers.find((candidate2) => candidate2.token === className2 || candidate2.useClass === className2);
|
|
958
|
+
const scope = provider?.scope ?? injectable?.scope ?? "job";
|
|
959
|
+
if (scope === "request") {
|
|
960
|
+
ctx.diagnostics.push({
|
|
961
|
+
severity: "error",
|
|
962
|
+
code: "invalid-job-scope",
|
|
963
|
+
message: `job ${className2} 不能使用 request scope;Job 只能使用 application 或 job scope`,
|
|
964
|
+
file: sourcePath(ctx.rootDir, cls.getSourceFile().fileName),
|
|
965
|
+
line: lineOf(cls),
|
|
966
|
+
suggestion: "移除 request scope,或改用 application/job scope。",
|
|
967
|
+
errorCode: "SC4007",
|
|
968
|
+
docsUrl: "https://supacloud.dev/errors/SC4007"
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
const aspects2 = parseAspectRefs(getProp(meta, "aspects"), ctx, `job ${className2}`);
|
|
972
|
+
jobs.push({
|
|
973
|
+
className: className2,
|
|
974
|
+
name: stringLiteralProp(meta, "name") ?? className2,
|
|
975
|
+
serviceKey: camelName(provider?.token ?? className2),
|
|
976
|
+
scope,
|
|
977
|
+
...aspects2.length > 0 ? { aspects: aspects2 } : {}
|
|
432
978
|
});
|
|
433
979
|
}
|
|
434
980
|
}
|
|
@@ -437,8 +983,8 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
437
983
|
const meta = decoratorObjectArg(queryDec);
|
|
438
984
|
if (meta) {
|
|
439
985
|
queries.push({
|
|
440
|
-
className: cls.
|
|
441
|
-
name: stringLiteralProp(meta, "name") ?? cls.
|
|
986
|
+
className: cls.name?.text ?? "<anonymous>",
|
|
987
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
442
988
|
});
|
|
443
989
|
}
|
|
444
990
|
}
|
|
@@ -453,7 +999,9 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
453
999
|
providers,
|
|
454
1000
|
controllers,
|
|
455
1001
|
commands,
|
|
1002
|
+
jobs,
|
|
456
1003
|
queries,
|
|
1004
|
+
...aspects.length > 0 ? { aspects } : {},
|
|
457
1005
|
exports
|
|
458
1006
|
};
|
|
459
1007
|
}
|
|
@@ -462,14 +1010,14 @@ function commandModeProp(object, name) {
|
|
|
462
1010
|
return value === "required" || value === "none" ? value : undefined;
|
|
463
1011
|
}
|
|
464
1012
|
function parseProvider(el, exportsSet, ctx) {
|
|
465
|
-
const file = sourcePath(ctx.rootDir, el.getSourceFile().
|
|
466
|
-
const line = el
|
|
1013
|
+
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
1014
|
+
const line = lineOf(el);
|
|
467
1015
|
const unwrappedEl = unwrapForwardRef(el);
|
|
468
|
-
if (
|
|
469
|
-
const decl = resolveDeclaration(unwrappedEl)[0];
|
|
470
|
-
const cls = decl &&
|
|
471
|
-
const className = cls?.
|
|
472
|
-
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], missing: false };
|
|
1016
|
+
if (ts3.isIdentifier(unwrappedEl)) {
|
|
1017
|
+
const decl = resolveDeclaration(unwrappedEl, ctx)[0];
|
|
1018
|
+
const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
|
|
1019
|
+
const className = cls?.name?.text ?? unwrappedEl.text;
|
|
1020
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = cls ? classDeps(cls, ctx) : { deps: [], optionalDeps: [], selfDeps: [], skipSelfDeps: [], hostDeps: [], functionalInjects: [], missing: false };
|
|
473
1021
|
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
474
1022
|
if (missing) {
|
|
475
1023
|
warn(ctx, "missing-deps", `provider ${className} 的部分构造依赖无法静态解析`, file, line);
|
|
@@ -485,15 +1033,16 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
485
1033
|
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
486
1034
|
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
487
1035
|
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
1036
|
+
functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
|
|
488
1037
|
providedIn: injectable?.providedIn,
|
|
489
|
-
hasOnDestroy: cls
|
|
1038
|
+
hasOnDestroy: cls ? hasDestroyHook(cls) || undefined : undefined,
|
|
490
1039
|
exported: exportsSet.has(className),
|
|
491
1040
|
file,
|
|
492
1041
|
line,
|
|
493
|
-
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().
|
|
1042
|
+
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
|
|
494
1043
|
};
|
|
495
1044
|
}
|
|
496
|
-
if (!
|
|
1045
|
+
if (!ts3.isObjectLiteralExpression(el))
|
|
497
1046
|
return;
|
|
498
1047
|
const provideExpr = getProp(el, "provide");
|
|
499
1048
|
if (!provideExpr)
|
|
@@ -508,26 +1057,36 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
508
1057
|
const useExistingExpr = getProp(el, "useExisting");
|
|
509
1058
|
if (useClassExpr) {
|
|
510
1059
|
const unwrappedClass = unwrapForwardRef(useClassExpr);
|
|
511
|
-
const decl =
|
|
512
|
-
const cls = decl &&
|
|
513
|
-
const useClass = cls?.
|
|
1060
|
+
const decl = ts3.isIdentifier(unwrappedClass) ? resolveDeclaration(unwrappedClass, ctx)[0] : undefined;
|
|
1061
|
+
const cls = decl && ts3.isClassDeclaration(decl) ? decl : undefined;
|
|
1062
|
+
const useClass = cls?.name?.text ?? nodeText(unwrappedClass);
|
|
514
1063
|
let deps = explicitDeps;
|
|
515
1064
|
let optionalDeps = [];
|
|
516
1065
|
let selfDeps = [];
|
|
517
1066
|
let skipSelfDeps = [];
|
|
518
1067
|
let hostDeps = [];
|
|
519
|
-
|
|
1068
|
+
let functionalInjects = [];
|
|
1069
|
+
if (cls) {
|
|
520
1070
|
const result = classDeps(cls, ctx);
|
|
521
|
-
deps
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1071
|
+
if (deps.length === 0) {
|
|
1072
|
+
deps = result.deps;
|
|
1073
|
+
optionalDeps = result.optionalDeps;
|
|
1074
|
+
selfDeps = result.selfDeps;
|
|
1075
|
+
skipSelfDeps = result.skipSelfDeps;
|
|
1076
|
+
hostDeps = result.hostDeps;
|
|
1077
|
+
} else {
|
|
1078
|
+
optionalDeps = result.optionalDeps.filter((dep) => deps.includes(dep));
|
|
1079
|
+
selfDeps = result.selfDeps.filter((dep) => deps.includes(dep));
|
|
1080
|
+
skipSelfDeps = result.skipSelfDeps.filter((dep) => deps.includes(dep));
|
|
1081
|
+
hostDeps = result.hostDeps.filter((dep) => deps.includes(dep));
|
|
1082
|
+
}
|
|
1083
|
+
functionalInjects = result.functionalInjects;
|
|
526
1084
|
if (result.missing) {
|
|
527
1085
|
warn(ctx, "missing-deps", `provider ${token} (useClass ${useClass}) 的部分构造依赖无法静态解析`, file, line);
|
|
528
1086
|
}
|
|
529
1087
|
}
|
|
530
1088
|
const injectable = cls ? parseInjectableOptions(cls, ctx) : undefined;
|
|
1089
|
+
validateProviderCompatibility(provideExpr, useClassExpr, "class", token, ctx, file, line);
|
|
531
1090
|
return {
|
|
532
1091
|
token,
|
|
533
1092
|
tokenKind,
|
|
@@ -539,35 +1098,38 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
539
1098
|
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
540
1099
|
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
541
1100
|
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
1101
|
+
functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
|
|
542
1102
|
multi: multi ?? undefined,
|
|
543
1103
|
providedIn: injectable?.providedIn,
|
|
544
|
-
hasOnDestroy: cls
|
|
1104
|
+
hasOnDestroy: cls ? hasMethod(cls, "onDestroy") || undefined : undefined,
|
|
545
1105
|
exported: exportsSet.has(token),
|
|
546
1106
|
file,
|
|
547
1107
|
line,
|
|
548
|
-
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().
|
|
1108
|
+
importPath: cls ? modulePath(ctx.rootDir, cls.getSourceFile().fileName) : undefined
|
|
549
1109
|
};
|
|
550
1110
|
}
|
|
551
1111
|
if (useValueExpr) {
|
|
1112
|
+
validateProviderCompatibility(provideExpr, useValueExpr, "value", token, ctx, file, line);
|
|
552
1113
|
return {
|
|
553
1114
|
token,
|
|
554
1115
|
tokenKind,
|
|
555
1116
|
kind: "value",
|
|
556
|
-
useValueExpr: useValueExpr
|
|
1117
|
+
useValueExpr: nodeText(useValueExpr),
|
|
557
1118
|
scope: resolveScope({ explicit: explicitScope, tokenName: token }, ctx),
|
|
558
1119
|
deps: [],
|
|
559
1120
|
multi: multi ?? undefined,
|
|
560
1121
|
exported: exportsSet.has(token),
|
|
561
1122
|
file,
|
|
562
1123
|
line,
|
|
563
|
-
importPath:
|
|
1124
|
+
importPath: ts3.isIdentifier(useValueExpr) ? importPathOf(useValueExpr, ctx) : undefined
|
|
564
1125
|
};
|
|
565
1126
|
}
|
|
566
1127
|
if (useFactoryExpr) {
|
|
567
|
-
const factoryName =
|
|
568
|
-
const decl = resolveDeclaration(useFactoryExpr)[0];
|
|
569
|
-
return decl && (
|
|
570
|
-
})() : useFactoryExpr
|
|
1128
|
+
const factoryName = ts3.isIdentifier(useFactoryExpr) ? (() => {
|
|
1129
|
+
const decl = resolveDeclaration(useFactoryExpr, ctx)[0];
|
|
1130
|
+
return decl && (ts3.isFunctionDeclaration(decl) || ts3.isVariableDeclaration(decl)) ? (ts3.isFunctionDeclaration(decl) ? decl.name?.text : variableName(decl)) ?? useFactoryExpr.text : useFactoryExpr.text;
|
|
1131
|
+
})() : nodeText(useFactoryExpr);
|
|
1132
|
+
validateProviderCompatibility(provideExpr, useFactoryExpr, "factory", token, ctx, file, line);
|
|
571
1133
|
return {
|
|
572
1134
|
token,
|
|
573
1135
|
tokenKind,
|
|
@@ -579,11 +1141,12 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
579
1141
|
exported: exportsSet.has(token),
|
|
580
1142
|
file,
|
|
581
1143
|
line,
|
|
582
|
-
importPath:
|
|
1144
|
+
importPath: ts3.isIdentifier(useFactoryExpr) ? importPathOf(useFactoryExpr, ctx) : undefined
|
|
583
1145
|
};
|
|
584
1146
|
}
|
|
585
1147
|
if (useExistingExpr) {
|
|
586
1148
|
const target = tokenNameOf(useExistingExpr, ctx).name;
|
|
1149
|
+
validateProviderCompatibility(provideExpr, useExistingExpr, "existing", token, ctx, file, line);
|
|
587
1150
|
return {
|
|
588
1151
|
token,
|
|
589
1152
|
tokenKind,
|
|
@@ -599,14 +1162,262 @@ function parseProvider(el, exportsSet, ctx) {
|
|
|
599
1162
|
}
|
|
600
1163
|
return;
|
|
601
1164
|
}
|
|
1165
|
+
function expandProviderExpressions(expressions, ctx, seen = new Set) {
|
|
1166
|
+
const result = [];
|
|
1167
|
+
for (const expression of expressions) {
|
|
1168
|
+
if (ts3.isSpreadElement(expression)) {
|
|
1169
|
+
result.push(...expandProviderExpressions([expression.expression], ctx, seen));
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (ts3.isIdentifier(expression)) {
|
|
1173
|
+
const declaration = resolveDeclaration(expression, ctx)[0];
|
|
1174
|
+
if (declaration && ts3.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
1175
|
+
const key = `${declaration.getSourceFile().fileName}:${declaration.pos}`;
|
|
1176
|
+
if (seen.has(key))
|
|
1177
|
+
continue;
|
|
1178
|
+
const initializer = declaration.initializer;
|
|
1179
|
+
if (ts3.isCallExpression(initializer) && isProviderHelper(initializer, "makeEnvironmentProviders")) {
|
|
1180
|
+
const nested = initializer.arguments[0];
|
|
1181
|
+
if (nested && ts3.isArrayLiteralExpression(nested)) {
|
|
1182
|
+
seen.add(key);
|
|
1183
|
+
result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
|
|
1184
|
+
seen.delete(key);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (ts3.isCallExpression(expression) && isProviderHelper(expression, "makeEnvironmentProviders")) {
|
|
1191
|
+
const nested = expression.arguments[0];
|
|
1192
|
+
if (nested && ts3.isArrayLiteralExpression(nested)) {
|
|
1193
|
+
result.push(...expandProviderExpressions([...nested.elements], ctx, seen));
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
result.push(expression);
|
|
1198
|
+
}
|
|
1199
|
+
return result;
|
|
1200
|
+
}
|
|
1201
|
+
function isProviderHelper(expression, name) {
|
|
1202
|
+
return nodeText(expression.expression).split(".").pop() === name;
|
|
1203
|
+
}
|
|
1204
|
+
function parseFunctionalProvider(expression, exportsSet, ctx) {
|
|
1205
|
+
if (!ts3.isCallExpression(expression))
|
|
1206
|
+
return;
|
|
1207
|
+
const helper = nodeText(expression.expression).split(".").pop();
|
|
1208
|
+
const args = expression.arguments;
|
|
1209
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
1210
|
+
const line = lineOf(expression);
|
|
1211
|
+
if (helper === "provideToken") {
|
|
1212
|
+
const tokenExpr = args[0];
|
|
1213
|
+
const valueExpr = args[1];
|
|
1214
|
+
if (!tokenExpr || !valueExpr)
|
|
1215
|
+
return [];
|
|
1216
|
+
const { name: token, kind: tokenKind } = tokenNameOf(tokenExpr, ctx);
|
|
1217
|
+
validateProviderCompatibility(tokenExpr, valueExpr, "value", token, ctx, file, line);
|
|
1218
|
+
return [{
|
|
1219
|
+
token,
|
|
1220
|
+
tokenKind,
|
|
1221
|
+
kind: "value",
|
|
1222
|
+
useValueExpr: nodeText(valueExpr),
|
|
1223
|
+
scope: resolveScope({ tokenName: token }, ctx),
|
|
1224
|
+
deps: [],
|
|
1225
|
+
exported: exportsSet.has(token),
|
|
1226
|
+
file,
|
|
1227
|
+
line,
|
|
1228
|
+
importPath: ts3.isIdentifier(valueExpr) ? importPathOf(valueExpr, ctx) : undefined
|
|
1229
|
+
}];
|
|
1230
|
+
}
|
|
1231
|
+
if (helper === "provideAppInitializer" || helper === "provideEnvironmentInitializer") {
|
|
1232
|
+
const initializer = args[0];
|
|
1233
|
+
if (!initializer)
|
|
1234
|
+
return [];
|
|
1235
|
+
const token = helper === "provideAppInitializer" ? "APP_INITIALIZER" : "ENVIRONMENT_INITIALIZER";
|
|
1236
|
+
return [{
|
|
1237
|
+
token,
|
|
1238
|
+
tokenKind: "injection-token",
|
|
1239
|
+
kind: "value",
|
|
1240
|
+
useValueExpr: nodeText(initializer),
|
|
1241
|
+
scope: "application",
|
|
1242
|
+
deps: [],
|
|
1243
|
+
multi: true,
|
|
1244
|
+
exported: false,
|
|
1245
|
+
file,
|
|
1246
|
+
line,
|
|
1247
|
+
importPath: ts3.isIdentifier(initializer) ? importPathOf(initializer, ctx) : undefined
|
|
1248
|
+
}];
|
|
1249
|
+
}
|
|
1250
|
+
if (helper === "provideRouter") {
|
|
1251
|
+
const providers = [];
|
|
1252
|
+
const routes = args[0];
|
|
1253
|
+
if (routes) {
|
|
1254
|
+
providers.push({
|
|
1255
|
+
token: "ROUTE_CONFIG",
|
|
1256
|
+
tokenKind: "injection-token",
|
|
1257
|
+
kind: "value",
|
|
1258
|
+
useValueExpr: nodeText(routes),
|
|
1259
|
+
scope: "application",
|
|
1260
|
+
deps: [],
|
|
1261
|
+
exported: false,
|
|
1262
|
+
file,
|
|
1263
|
+
line,
|
|
1264
|
+
importPath: ts3.isIdentifier(routes) ? importPathOf(routes, ctx) : undefined
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
for (const feature of args.slice(1)) {
|
|
1268
|
+
if (!ts3.isCallExpression(feature))
|
|
1269
|
+
continue;
|
|
1270
|
+
const featureName = nodeText(feature.expression).split(".").pop();
|
|
1271
|
+
if (featureName === "withRouterConfig" && feature.arguments[0]) {
|
|
1272
|
+
providers.push({
|
|
1273
|
+
token: "ROUTER_CONFIGURATION",
|
|
1274
|
+
tokenKind: "injection-token",
|
|
1275
|
+
kind: "value",
|
|
1276
|
+
useValueExpr: nodeText(feature.arguments[0]),
|
|
1277
|
+
scope: "application",
|
|
1278
|
+
deps: [],
|
|
1279
|
+
exported: false,
|
|
1280
|
+
file,
|
|
1281
|
+
line
|
|
1282
|
+
});
|
|
1283
|
+
} else if (featureName === "withTitleStrategy" && feature.arguments[0]) {
|
|
1284
|
+
const strategy = feature.arguments[0];
|
|
1285
|
+
const isClass = ts3.isIdentifier(strategy) && Boolean(resolveDeclaration(strategy, ctx).find((declaration) => ts3.isClassDeclaration(declaration)));
|
|
1286
|
+
providers.push({
|
|
1287
|
+
token: "TITLE_STRATEGY",
|
|
1288
|
+
tokenKind: "injection-token",
|
|
1289
|
+
kind: isClass ? "class" : "value",
|
|
1290
|
+
...isClass ? { useClass: nodeText(strategy) } : { useValueExpr: nodeText(strategy) },
|
|
1291
|
+
scope: "application",
|
|
1292
|
+
deps: [],
|
|
1293
|
+
exported: false,
|
|
1294
|
+
file,
|
|
1295
|
+
line,
|
|
1296
|
+
importPath: ts3.isIdentifier(strategy) ? importPathOf(strategy, ctx) : undefined
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return providers;
|
|
1301
|
+
}
|
|
1302
|
+
if (helper === "provideHttpClient") {
|
|
1303
|
+
const providers = [{
|
|
1304
|
+
token: "HttpClient",
|
|
1305
|
+
tokenKind: "class",
|
|
1306
|
+
kind: "class",
|
|
1307
|
+
useClass: "HttpClient",
|
|
1308
|
+
scope: "application",
|
|
1309
|
+
deps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
|
|
1310
|
+
optionalDeps: ["HTTP_CLIENT_CONFIG", "HTTP_INTERCEPTORS"],
|
|
1311
|
+
exported: false,
|
|
1312
|
+
file,
|
|
1313
|
+
line,
|
|
1314
|
+
importModule: "@supacloud/app"
|
|
1315
|
+
}];
|
|
1316
|
+
for (const feature of args) {
|
|
1317
|
+
if (!ts3.isCallExpression(feature))
|
|
1318
|
+
continue;
|
|
1319
|
+
const featureName = nodeText(feature.expression).split(".").pop();
|
|
1320
|
+
if (featureName === "withInterceptors") {
|
|
1321
|
+
for (const interceptorArg of feature.arguments) {
|
|
1322
|
+
const values = ts3.isArrayLiteralExpression(interceptorArg) ? [...interceptorArg.elements] : [interceptorArg];
|
|
1323
|
+
for (const value of values) {
|
|
1324
|
+
providers.push({
|
|
1325
|
+
token: "HTTP_INTERCEPTORS",
|
|
1326
|
+
tokenKind: "injection-token",
|
|
1327
|
+
kind: "value",
|
|
1328
|
+
useValueExpr: nodeText(value),
|
|
1329
|
+
scope: "application",
|
|
1330
|
+
deps: [],
|
|
1331
|
+
multi: true,
|
|
1332
|
+
exported: false,
|
|
1333
|
+
file,
|
|
1334
|
+
line,
|
|
1335
|
+
importPath: ts3.isIdentifier(value) ? importPathOf(value, ctx) : undefined
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} else if (featureName === "withFetch" && feature.arguments.length > 0) {
|
|
1340
|
+
warn(ctx, "unsupported-provider-helper", "provideHttpClient(withFetch(customFetch)) 需要显式声明 HTTP_CLIENT_CONFIG provider 才能保持静态生成", file, line);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return providers;
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
function validateProviderCompatibility(provideExpr, implementationExpr, kind, tokenName, ctx, file, line) {
|
|
1348
|
+
const expected = providerTokenValueType(provideExpr, ctx);
|
|
1349
|
+
const actual = providerImplementationType(implementationExpr, kind, ctx);
|
|
1350
|
+
if (!expected || !actual || isUnknownOrAny(expected) || isUnknownOrAny(actual))
|
|
1351
|
+
return;
|
|
1352
|
+
if (ctx.checker.isTypeAssignableTo(actual, expected))
|
|
1353
|
+
return;
|
|
1354
|
+
const providerKind = kind === "class" ? "useClass" : `use${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
|
|
1355
|
+
ctx.diagnostics.push({
|
|
1356
|
+
severity: "error",
|
|
1357
|
+
code: "provider-type-mismatch",
|
|
1358
|
+
message: `Provider '${tokenName}' 的 ${providerKind} 类型不满足 Token 契约:需要 ${ctx.checker.typeToString(expected, provideExpr)},实际为 ${ctx.checker.typeToString(actual, implementationExpr)}`,
|
|
1359
|
+
file,
|
|
1360
|
+
line,
|
|
1361
|
+
errorCode: "SC2010",
|
|
1362
|
+
docsUrl: "https://supacloud.dev/errors/SC2010"
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
function providerTokenValueType(expr, ctx) {
|
|
1366
|
+
const type = ctx.checker.getTypeAtLocation(expr);
|
|
1367
|
+
const typeArguments = typeArgumentsOf(type, ctx);
|
|
1368
|
+
if (typeArguments.length > 0)
|
|
1369
|
+
return typeArguments[0];
|
|
1370
|
+
if (ts3.isIdentifier(expr)) {
|
|
1371
|
+
const declaration = resolveDeclaration(expr, ctx)[0];
|
|
1372
|
+
if (declaration && ts3.isClassDeclaration(declaration)) {
|
|
1373
|
+
return declaredClassType(declaration, ctx);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
function providerImplementationType(expr, kind, ctx) {
|
|
1379
|
+
if (kind === "class" || kind === "existing") {
|
|
1380
|
+
if (ts3.isIdentifier(expr)) {
|
|
1381
|
+
const declaration = resolveDeclaration(expr, ctx)[0];
|
|
1382
|
+
if (declaration && ts3.isClassDeclaration(declaration)) {
|
|
1383
|
+
return declaredClassType(declaration, ctx);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const type = ctx.checker.getTypeAtLocation(expr);
|
|
1387
|
+
const typeArguments = typeArgumentsOf(type, ctx);
|
|
1388
|
+
return typeArguments.length > 0 ? typeArguments[0] : undefined;
|
|
1389
|
+
}
|
|
1390
|
+
if (kind === "factory") {
|
|
1391
|
+
const type = ctx.checker.getTypeAtLocation(expr);
|
|
1392
|
+
const signature = ctx.checker.getSignaturesOfType(type, ts3.SignatureKind.Call)[0];
|
|
1393
|
+
return signature?.getReturnType();
|
|
1394
|
+
}
|
|
1395
|
+
return ctx.checker.getTypeAtLocation(expr);
|
|
1396
|
+
}
|
|
1397
|
+
function declaredClassType(declaration, ctx) {
|
|
1398
|
+
const name = declaration.name;
|
|
1399
|
+
if (!name)
|
|
1400
|
+
return;
|
|
1401
|
+
const symbol = ctx.checker.getSymbolAtLocation(name);
|
|
1402
|
+
return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
|
|
1403
|
+
}
|
|
1404
|
+
function typeArgumentsOf(type, ctx) {
|
|
1405
|
+
return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
|
|
1406
|
+
}
|
|
1407
|
+
function isTypeReference(type) {
|
|
1408
|
+
return "target" in type;
|
|
1409
|
+
}
|
|
1410
|
+
function isUnknownOrAny(type) {
|
|
1411
|
+
return (type.flags & (ts3.TypeFlags.Any | ts3.TypeFlags.Unknown)) !== 0;
|
|
1412
|
+
}
|
|
602
1413
|
function parseController(input, ctx) {
|
|
603
1414
|
let decl;
|
|
604
|
-
if (
|
|
1415
|
+
if (ts3.isClassDeclaration(input)) {
|
|
605
1416
|
decl = input;
|
|
606
1417
|
} else {
|
|
607
1418
|
const unwrapped = unwrapForwardRef(input);
|
|
608
|
-
const resolved =
|
|
609
|
-
if (resolved &&
|
|
1419
|
+
const resolved = ts3.isIdentifier(unwrapped) ? resolveDeclaration(unwrapped, ctx)[0] : undefined;
|
|
1420
|
+
if (resolved && ts3.isClassDeclaration(resolved)) {
|
|
610
1421
|
decl = resolved;
|
|
611
1422
|
}
|
|
612
1423
|
}
|
|
@@ -617,46 +1428,46 @@ function parseController(input, ctx) {
|
|
|
617
1428
|
return;
|
|
618
1429
|
let path = "/";
|
|
619
1430
|
let standalone;
|
|
620
|
-
const pathArg = controllerDec
|
|
1431
|
+
const pathArg = decoratorArguments(controllerDec)[0];
|
|
621
1432
|
if (pathArg) {
|
|
622
|
-
if (
|
|
623
|
-
path = pathArg.
|
|
624
|
-
} else if (
|
|
1433
|
+
if (ts3.isStringLiteral(pathArg)) {
|
|
1434
|
+
path = pathArg.text;
|
|
1435
|
+
} else if (ts3.isObjectLiteralExpression(pathArg)) {
|
|
625
1436
|
const p = stringLiteralProp(pathArg, "path");
|
|
626
1437
|
if (p)
|
|
627
1438
|
path = p;
|
|
628
1439
|
standalone = booleanProp(pathArg, "standalone");
|
|
629
1440
|
}
|
|
630
1441
|
}
|
|
631
|
-
const { deps, optionalDeps, selfDeps, skipSelfDeps, missing } = classDeps(decl, ctx);
|
|
632
|
-
const file = sourcePath(ctx.rootDir, decl.getSourceFile().
|
|
1442
|
+
const { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing } = classDeps(decl, ctx);
|
|
1443
|
+
const file = sourcePath(ctx.rootDir, decl.getSourceFile().fileName);
|
|
633
1444
|
if (missing) {
|
|
634
|
-
warn(ctx, "missing-deps", `controller ${decl.
|
|
1445
|
+
warn(ctx, "missing-deps", `controller ${decl.name?.text} 的部分构造依赖无法静态解析`, file, lineOf(decl));
|
|
635
1446
|
}
|
|
636
1447
|
const injectable = parseInjectableOptions(decl, ctx);
|
|
637
1448
|
const routes = [];
|
|
638
1449
|
const schemaImports = {};
|
|
639
1450
|
const classGuards = [];
|
|
640
|
-
for (const dec of decl
|
|
641
|
-
if (
|
|
642
|
-
for (const gArg of dec
|
|
643
|
-
classGuards.push(tokenText(gArg));
|
|
1451
|
+
for (const dec of decoratorsOf(decl)) {
|
|
1452
|
+
if (decoratorName2(dec) === "UseGuards") {
|
|
1453
|
+
for (const gArg of decoratorArguments(dec)) {
|
|
1454
|
+
classGuards.push(tokenText(gArg, ctx));
|
|
644
1455
|
}
|
|
645
1456
|
}
|
|
646
1457
|
}
|
|
647
|
-
for (const method of decl.
|
|
648
|
-
for (const dec of method
|
|
649
|
-
const name =
|
|
1458
|
+
for (const method of decl.members.filter(ts3.isMethodDeclaration)) {
|
|
1459
|
+
for (const dec of decoratorsOf(method)) {
|
|
1460
|
+
const name = decoratorName2(dec);
|
|
650
1461
|
const httpMethod = name ? ROUTE_DECORATORS[name] : undefined;
|
|
651
1462
|
if (!httpMethod)
|
|
652
1463
|
continue;
|
|
653
|
-
const args = dec
|
|
1464
|
+
const args = decoratorArguments(dec);
|
|
654
1465
|
const pathArg2 = args[0];
|
|
655
|
-
const routePath = pathArg2 &&
|
|
1466
|
+
const routePath = pathArg2 && ts3.isStringLiteral(pathArg2) ? pathArg2.text : "/";
|
|
656
1467
|
const route = {
|
|
657
1468
|
method: httpMethod,
|
|
658
1469
|
path: routePath,
|
|
659
|
-
handler: method.
|
|
1470
|
+
handler: propertyName(method.name)
|
|
660
1471
|
};
|
|
661
1472
|
const pathParams = [];
|
|
662
1473
|
const paramRegex = /:([a-zA-Z0-9_]+)/g;
|
|
@@ -674,13 +1485,13 @@ function parseController(input, ctx) {
|
|
|
674
1485
|
const queryDefaults = {};
|
|
675
1486
|
let hasBodyBinding = false;
|
|
676
1487
|
const handlerParams = [];
|
|
677
|
-
for (const p of method.
|
|
678
|
-
const pName = p
|
|
1488
|
+
for (const p of method.parameters) {
|
|
1489
|
+
const pName = parameterName(p);
|
|
679
1490
|
let hasBindingDecorator = false;
|
|
680
1491
|
let paramNode;
|
|
681
|
-
for (const pDec of p
|
|
682
|
-
const dName =
|
|
683
|
-
const dArgs = pDec
|
|
1492
|
+
for (const pDec of decoratorsOf(p)) {
|
|
1493
|
+
const dName = decoratorName2(pDec);
|
|
1494
|
+
const dArgs = decoratorArguments(pDec);
|
|
684
1495
|
if (dName === "Param") {
|
|
685
1496
|
hasBindingDecorator = true;
|
|
686
1497
|
const parsed = parseBindingOptions(dArgs, pName);
|
|
@@ -722,7 +1533,7 @@ function parseController(input, ctx) {
|
|
|
722
1533
|
}
|
|
723
1534
|
if (!hasBindingDecorator && pathParams.includes(pName)) {
|
|
724
1535
|
paramBindings.push(pName);
|
|
725
|
-
const typeText = p.
|
|
1536
|
+
const typeText = p.type ? nodeText(p.type) : "";
|
|
726
1537
|
let inferredTransform;
|
|
727
1538
|
if (typeText === "number") {
|
|
728
1539
|
paramTransforms[pName] = "number";
|
|
@@ -765,37 +1576,37 @@ function parseController(input, ctx) {
|
|
|
765
1576
|
route.handlerParams = handlerParams;
|
|
766
1577
|
const routeGuards = [...classGuards];
|
|
767
1578
|
const routeCanDeactivate = [];
|
|
768
|
-
for (const mDec of method
|
|
769
|
-
const dName =
|
|
770
|
-
const mArgs = mDec
|
|
1579
|
+
for (const mDec of decoratorsOf(method)) {
|
|
1580
|
+
const dName = decoratorName2(mDec);
|
|
1581
|
+
const mArgs = decoratorArguments(mDec);
|
|
771
1582
|
if (dName === "UseGuards") {
|
|
772
1583
|
for (const gArg of mArgs) {
|
|
773
|
-
routeGuards.push(tokenText(gArg));
|
|
1584
|
+
routeGuards.push(tokenText(gArg, ctx));
|
|
774
1585
|
}
|
|
775
1586
|
} else if (dName === "CanDeactivate") {
|
|
776
1587
|
for (const gArg of mArgs) {
|
|
777
|
-
routeCanDeactivate.push(tokenText(gArg));
|
|
1588
|
+
routeCanDeactivate.push(tokenText(gArg, ctx));
|
|
778
1589
|
}
|
|
779
1590
|
} else if (dName === "Title") {
|
|
780
1591
|
const tArg = mArgs[0];
|
|
781
|
-
if (tArg &&
|
|
782
|
-
route.title = tArg.
|
|
1592
|
+
if (tArg && ts3.isStringLiteral(tArg)) {
|
|
1593
|
+
route.title = tArg.text;
|
|
783
1594
|
}
|
|
784
1595
|
} else if (dName === "Data") {
|
|
785
1596
|
const dArg = mArgs[0];
|
|
786
|
-
if (dArg &&
|
|
1597
|
+
if (dArg && ts3.isObjectLiteralExpression(dArg)) {
|
|
787
1598
|
route.data = { ...route.data, ...parseObjectLiteralValues(dArg) };
|
|
788
1599
|
}
|
|
789
1600
|
} else if (dName === "Resolve") {
|
|
790
1601
|
const rArg = mArgs[0];
|
|
791
|
-
if (rArg &&
|
|
1602
|
+
if (rArg && ts3.isObjectLiteralExpression(rArg)) {
|
|
792
1603
|
const resolvers = route.resolvers ?? {};
|
|
793
|
-
for (const prop of rArg.
|
|
794
|
-
if (
|
|
795
|
-
const rName = prop.
|
|
796
|
-
const init = prop.
|
|
1604
|
+
for (const prop of rArg.properties) {
|
|
1605
|
+
if (ts3.isPropertyAssignment(prop)) {
|
|
1606
|
+
const rName = propertyName(prop.name);
|
|
1607
|
+
const init = prop.initializer;
|
|
797
1608
|
if (init)
|
|
798
|
-
resolvers[rName] = tokenText(init);
|
|
1609
|
+
resolvers[rName] = tokenText(init, ctx);
|
|
799
1610
|
}
|
|
800
1611
|
}
|
|
801
1612
|
if (Object.keys(resolvers).length > 0) {
|
|
@@ -805,52 +1616,52 @@ function parseController(input, ctx) {
|
|
|
805
1616
|
}
|
|
806
1617
|
}
|
|
807
1618
|
const optionsArg = args[1];
|
|
808
|
-
if (optionsArg &&
|
|
1619
|
+
if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
|
|
809
1620
|
for (const field of ["body", "params", "query", "response"]) {
|
|
810
1621
|
const schemaExpr = getProp(optionsArg, field);
|
|
811
|
-
if (schemaExpr &&
|
|
812
|
-
route[field] = schemaExpr
|
|
1622
|
+
if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
|
|
1623
|
+
route[field] = nodeText(schemaExpr);
|
|
813
1624
|
const importPath = importPathOf(schemaExpr, ctx);
|
|
814
1625
|
if (importPath)
|
|
815
|
-
schemaImports[schemaExpr.
|
|
1626
|
+
schemaImports[schemaExpr.text] = importPath;
|
|
816
1627
|
}
|
|
817
1628
|
}
|
|
818
1629
|
const commandExpr = getProp(optionsArg, "command");
|
|
819
|
-
if (commandExpr &&
|
|
820
|
-
const commandDecl = resolveDeclaration(commandExpr)[0];
|
|
821
|
-
route.command = commandDecl &&
|
|
1630
|
+
if (commandExpr && ts3.isIdentifier(commandExpr)) {
|
|
1631
|
+
const commandDecl = resolveDeclaration(commandExpr, ctx)[0];
|
|
1632
|
+
route.command = commandDecl && ts3.isClassDeclaration(commandDecl) ? commandDecl.name?.text ?? commandExpr.text : commandExpr.text;
|
|
822
1633
|
}
|
|
823
1634
|
const guardsExpr = getProp(optionsArg, "guards");
|
|
824
|
-
if (guardsExpr &&
|
|
825
|
-
for (const el of guardsExpr.
|
|
826
|
-
routeGuards.push(tokenText(el));
|
|
1635
|
+
if (guardsExpr && ts3.isArrayLiteralExpression(guardsExpr)) {
|
|
1636
|
+
for (const el of guardsExpr.elements) {
|
|
1637
|
+
routeGuards.push(tokenText(el, ctx));
|
|
827
1638
|
}
|
|
828
1639
|
}
|
|
829
1640
|
const canMatchExpr = getProp(optionsArg, "canMatch");
|
|
830
|
-
if (canMatchExpr &&
|
|
1641
|
+
if (canMatchExpr && ts3.isArrayLiteralExpression(canMatchExpr)) {
|
|
831
1642
|
const canMatchList = [];
|
|
832
|
-
for (const el of canMatchExpr.
|
|
833
|
-
canMatchList.push(tokenText(el));
|
|
1643
|
+
for (const el of canMatchExpr.elements) {
|
|
1644
|
+
canMatchList.push(tokenText(el, ctx));
|
|
834
1645
|
}
|
|
835
1646
|
if (canMatchList.length > 0) {
|
|
836
1647
|
route.canMatch = canMatchList;
|
|
837
1648
|
}
|
|
838
1649
|
}
|
|
839
1650
|
const canDeactivateExpr = getProp(optionsArg, "canDeactivate");
|
|
840
|
-
if (canDeactivateExpr &&
|
|
841
|
-
for (const el of canDeactivateExpr.
|
|
842
|
-
routeCanDeactivate.push(tokenText(el));
|
|
1651
|
+
if (canDeactivateExpr && ts3.isArrayLiteralExpression(canDeactivateExpr)) {
|
|
1652
|
+
for (const el of canDeactivateExpr.elements) {
|
|
1653
|
+
routeCanDeactivate.push(tokenText(el, ctx));
|
|
843
1654
|
}
|
|
844
1655
|
}
|
|
845
1656
|
const resolversExpr = getProp(optionsArg, "resolvers");
|
|
846
|
-
if (resolversExpr &&
|
|
1657
|
+
if (resolversExpr && ts3.isObjectLiteralExpression(resolversExpr)) {
|
|
847
1658
|
const resolvers = {};
|
|
848
|
-
for (const prop of resolversExpr.
|
|
849
|
-
if (
|
|
850
|
-
const rName = prop.
|
|
851
|
-
const init = prop.
|
|
1659
|
+
for (const prop of resolversExpr.properties) {
|
|
1660
|
+
if (ts3.isPropertyAssignment(prop)) {
|
|
1661
|
+
const rName = propertyName(prop.name);
|
|
1662
|
+
const init = prop.initializer;
|
|
852
1663
|
if (init)
|
|
853
|
-
resolvers[rName] = tokenText(init);
|
|
1664
|
+
resolvers[rName] = tokenText(init, ctx);
|
|
854
1665
|
}
|
|
855
1666
|
}
|
|
856
1667
|
if (Object.keys(resolvers).length > 0) {
|
|
@@ -858,24 +1669,27 @@ function parseController(input, ctx) {
|
|
|
858
1669
|
}
|
|
859
1670
|
}
|
|
860
1671
|
const redirectToExpr = getProp(optionsArg, "redirectTo");
|
|
861
|
-
if (redirectToExpr &&
|
|
862
|
-
route.redirectTo = redirectToExpr.
|
|
1672
|
+
if (redirectToExpr && ts3.isStringLiteral(redirectToExpr)) {
|
|
1673
|
+
route.redirectTo = redirectToExpr.text;
|
|
863
1674
|
}
|
|
864
1675
|
const pathMatchExpr = getProp(optionsArg, "pathMatch");
|
|
865
|
-
if (pathMatchExpr &&
|
|
866
|
-
const val = pathMatchExpr.
|
|
1676
|
+
if (pathMatchExpr && ts3.isStringLiteral(pathMatchExpr)) {
|
|
1677
|
+
const val = pathMatchExpr.text;
|
|
867
1678
|
if (val === "full" || val === "prefix") {
|
|
868
1679
|
route.pathMatch = val;
|
|
869
1680
|
}
|
|
870
1681
|
}
|
|
871
1682
|
const titleExpr = getProp(optionsArg, "title");
|
|
872
|
-
if (titleExpr &&
|
|
873
|
-
route.title = titleExpr.
|
|
1683
|
+
if (titleExpr && ts3.isStringLiteral(titleExpr)) {
|
|
1684
|
+
route.title = titleExpr.text;
|
|
874
1685
|
}
|
|
875
1686
|
const dataExpr = getProp(optionsArg, "data");
|
|
876
|
-
if (dataExpr &&
|
|
1687
|
+
if (dataExpr && ts3.isObjectLiteralExpression(dataExpr)) {
|
|
877
1688
|
route.data = { ...route.data, ...parseObjectLiteralValues(dataExpr) };
|
|
878
1689
|
}
|
|
1690
|
+
const aspects = parseAspectRefs(getProp(optionsArg, "aspects"), ctx, `route ${httpMethod} ${routePath}`);
|
|
1691
|
+
if (aspects.length > 0)
|
|
1692
|
+
route.aspects = aspects;
|
|
879
1693
|
}
|
|
880
1694
|
if (routeGuards.length > 0) {
|
|
881
1695
|
route.guards = routeGuards;
|
|
@@ -887,46 +1701,40 @@ function parseController(input, ctx) {
|
|
|
887
1701
|
}
|
|
888
1702
|
}
|
|
889
1703
|
return {
|
|
890
|
-
className: decl.
|
|
1704
|
+
className: decl.name?.text ?? "<anonymous>",
|
|
891
1705
|
path,
|
|
892
1706
|
scope: injectable?.scope ?? "request",
|
|
893
1707
|
deps,
|
|
1708
|
+
hasOnDestroy: hasDestroyHook(decl) || undefined,
|
|
894
1709
|
optionalDeps: optionalDeps.length > 0 ? optionalDeps : undefined,
|
|
895
1710
|
selfDeps: selfDeps.length > 0 ? selfDeps : undefined,
|
|
896
1711
|
skipSelfDeps: skipSelfDeps.length > 0 ? skipSelfDeps : undefined,
|
|
1712
|
+
hostDeps: hostDeps.length > 0 ? hostDeps : undefined,
|
|
1713
|
+
functionalInjects: functionalInjects.length > 0 ? functionalInjects : undefined,
|
|
897
1714
|
standalone: standalone || undefined,
|
|
898
1715
|
routes,
|
|
899
1716
|
file,
|
|
900
|
-
importPath: modulePath(ctx.rootDir, decl.getSourceFile().
|
|
1717
|
+
importPath: modulePath(ctx.rootDir, decl.getSourceFile().fileName),
|
|
901
1718
|
schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
|
|
902
1719
|
};
|
|
903
1720
|
}
|
|
904
1721
|
function classDeps(cls, ctx) {
|
|
905
1722
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
deps: injectable.deps,
|
|
909
|
-
optionalDeps: [],
|
|
910
|
-
selfDeps: [],
|
|
911
|
-
skipSelfDeps: [],
|
|
912
|
-
hostDeps: [],
|
|
913
|
-
missing: false
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
const ctor = cls.getConstructors()[0];
|
|
917
|
-
const deps = [];
|
|
1723
|
+
const ctor = cls.members.find(ts3.isConstructorDeclaration);
|
|
1724
|
+
const deps = injectable?.deps ? [...injectable.deps] : [];
|
|
918
1725
|
const optionalDeps = [];
|
|
919
1726
|
const selfDeps = [];
|
|
920
1727
|
const skipSelfDeps = [];
|
|
921
1728
|
const hostDeps = [];
|
|
1729
|
+
const functionalInjects = [];
|
|
922
1730
|
let missing = false;
|
|
923
|
-
if (ctor && ctor.
|
|
924
|
-
const injectParams = parseInjectParams(cls);
|
|
1731
|
+
if (!injectable?.deps && ctor && ctor.parameters.length > 0) {
|
|
1732
|
+
const injectParams = parseInjectParams(cls, ctx);
|
|
925
1733
|
const optionalIndices = parseOptionalParams(cls);
|
|
926
1734
|
const selfIndices = parseModifierParams(cls, "Self");
|
|
927
1735
|
const skipSelfIndices = parseModifierParams(cls, "SkipSelf");
|
|
928
1736
|
const hostIndices = parseModifierParams(cls, "Host");
|
|
929
|
-
ctor.
|
|
1737
|
+
ctor.parameters.forEach((param, index) => {
|
|
930
1738
|
const isOptional = optionalIndices.has(index);
|
|
931
1739
|
const injected = injectParams.get(index);
|
|
932
1740
|
const tokenName = injected ?? paramTypeTokenName(param, ctx);
|
|
@@ -946,47 +1754,62 @@ function classDeps(cls, ctx) {
|
|
|
946
1754
|
}
|
|
947
1755
|
});
|
|
948
1756
|
}
|
|
949
|
-
for (const prop of cls.
|
|
950
|
-
const init = prop.
|
|
951
|
-
if (init &&
|
|
952
|
-
const callName = init.
|
|
1757
|
+
for (const prop of cls.members.filter(ts3.isPropertyDeclaration)) {
|
|
1758
|
+
const init = prop.initializer;
|
|
1759
|
+
if (init && ts3.isCallExpression(init)) {
|
|
1760
|
+
const callName = nodeText(init.expression).split(".").pop();
|
|
953
1761
|
if (callName === "inject") {
|
|
954
|
-
const [tokenArg, optionsArg] = init.
|
|
1762
|
+
const [tokenArg, optionsArg] = init.arguments;
|
|
955
1763
|
if (tokenArg) {
|
|
956
|
-
const tokenName = tokenText(tokenArg);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1764
|
+
const tokenName = tokenText(tokenArg, ctx);
|
|
1765
|
+
const unwrappedToken = unwrapForwardRef(tokenArg);
|
|
1766
|
+
const known = ts3.isStringLiteral(unwrappedToken) || ts3.isIdentifier(unwrappedToken) && (ctx.tokensByName.has(tokenName) || ctx.classesByName.has(tokenName));
|
|
1767
|
+
if (!known) {
|
|
1768
|
+
missing = true;
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (!deps.includes(tokenName))
|
|
1772
|
+
deps.push(tokenName);
|
|
1773
|
+
const options = optionsArg && ts3.isObjectLiteralExpression(optionsArg) ? {
|
|
1774
|
+
optional: booleanProp(optionsArg, "optional") ?? false,
|
|
1775
|
+
self: booleanProp(optionsArg, "self") ?? false,
|
|
1776
|
+
skipSelf: booleanProp(optionsArg, "skipSelf") ?? false,
|
|
1777
|
+
host: booleanProp(optionsArg, "host") ?? false
|
|
1778
|
+
} : { optional: false, self: false, skipSelf: false, host: false };
|
|
1779
|
+
if (options.optional && !optionalDeps.includes(tokenName))
|
|
1780
|
+
optionalDeps.push(tokenName);
|
|
1781
|
+
if (options.self && !selfDeps.includes(tokenName))
|
|
1782
|
+
selfDeps.push(tokenName);
|
|
1783
|
+
if (options.skipSelf && !skipSelfDeps.includes(tokenName))
|
|
1784
|
+
skipSelfDeps.push(tokenName);
|
|
1785
|
+
if (options.host && !hostDeps.includes(tokenName))
|
|
1786
|
+
hostDeps.push(tokenName);
|
|
1787
|
+
if (!functionalInjects.some((entry) => entry.token === tokenName)) {
|
|
1788
|
+
functionalInjects.push({
|
|
1789
|
+
token: tokenName,
|
|
1790
|
+
expression: nodeText(unwrappedToken),
|
|
1791
|
+
importPath: ts3.isIdentifier(unwrappedToken) ? (() => {
|
|
1792
|
+
const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
|
|
1793
|
+
return declaration && isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? modulePath(ctx.rootDir, declaration.getSourceFile().fileName) : undefined;
|
|
1794
|
+
})() : undefined,
|
|
1795
|
+
importModule: ts3.isIdentifier(unwrappedToken) ? (() => {
|
|
1796
|
+
const declaration = resolveDeclaration(unwrappedToken, ctx)[0];
|
|
1797
|
+
return declaration && !isProjectSourcePath(declaration.getSourceFile().fileName, ctx.rootDir) ? importModuleOf(unwrappedToken, ctx) : undefined;
|
|
1798
|
+
})() : undefined,
|
|
1799
|
+
...options
|
|
1800
|
+
});
|
|
978
1801
|
}
|
|
979
1802
|
}
|
|
980
1803
|
}
|
|
981
1804
|
}
|
|
982
1805
|
}
|
|
983
|
-
return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, missing };
|
|
1806
|
+
return { deps, optionalDeps, selfDeps, skipSelfDeps, hostDeps, functionalInjects, missing };
|
|
984
1807
|
}
|
|
985
1808
|
function paramTypeTokenName(param, ctx) {
|
|
986
|
-
const typeNode = param.
|
|
1809
|
+
const typeNode = param.type;
|
|
987
1810
|
if (!typeNode)
|
|
988
1811
|
return;
|
|
989
|
-
const text = typeNode
|
|
1812
|
+
const text = nodeText(typeNode).replace(/<.*>$/, "").replace(/\[\]$/, "").trim();
|
|
990
1813
|
if (ctx.classesByName.has(text))
|
|
991
1814
|
return text;
|
|
992
1815
|
if (ctx.tokensByName.has(text))
|
|
@@ -1004,63 +1827,63 @@ function parseInjectableOptions(cls, ctx) {
|
|
|
1004
1827
|
const providedIn = stringLiteralProp(obj, "providedIn");
|
|
1005
1828
|
const depsExpr = getProp(obj, "deps");
|
|
1006
1829
|
return {
|
|
1007
|
-
scope: scope &&
|
|
1830
|
+
scope: scope && isScope(scope) ? scope : undefined,
|
|
1008
1831
|
providedIn: providedIn === "root" ? "root" : undefined,
|
|
1009
|
-
deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : el
|
|
1832
|
+
deps: depsExpr ? arrayProp(obj, "deps").map((el) => ctx ? tokenNameOf(el, ctx).name : nodeText(el)) : undefined
|
|
1010
1833
|
};
|
|
1011
1834
|
}
|
|
1012
|
-
function parseInjectParams(cls) {
|
|
1835
|
+
function parseInjectParams(cls, ctx) {
|
|
1013
1836
|
const result = new Map;
|
|
1014
|
-
const ctor = cls.
|
|
1837
|
+
const ctor = cls.members.find(ts3.isConstructorDeclaration);
|
|
1015
1838
|
if (!ctor)
|
|
1016
1839
|
return result;
|
|
1017
|
-
ctor.
|
|
1018
|
-
for (const dec of param
|
|
1019
|
-
if (
|
|
1840
|
+
ctor.parameters.forEach((param, index) => {
|
|
1841
|
+
for (const dec of decoratorsOf(param)) {
|
|
1842
|
+
if (decoratorName2(dec) !== "Inject")
|
|
1020
1843
|
continue;
|
|
1021
|
-
const arg = dec
|
|
1844
|
+
const arg = decoratorArguments(dec)[0];
|
|
1022
1845
|
if (arg)
|
|
1023
|
-
result.set(index, tokenText(arg));
|
|
1846
|
+
result.set(index, tokenText(arg, ctx));
|
|
1024
1847
|
}
|
|
1025
1848
|
});
|
|
1026
1849
|
return result;
|
|
1027
1850
|
}
|
|
1028
1851
|
function parseOptionalParams(cls) {
|
|
1029
1852
|
const result = new Set;
|
|
1030
|
-
const ctor = cls.
|
|
1853
|
+
const ctor = cls.members.find(ts3.isConstructorDeclaration);
|
|
1031
1854
|
if (!ctor)
|
|
1032
1855
|
return result;
|
|
1033
|
-
ctor.
|
|
1034
|
-
for (const dec of param
|
|
1035
|
-
if (
|
|
1856
|
+
ctor.parameters.forEach((param, index) => {
|
|
1857
|
+
for (const dec of decoratorsOf(param)) {
|
|
1858
|
+
if (decoratorName2(dec) === "Optional")
|
|
1036
1859
|
result.add(index);
|
|
1037
1860
|
}
|
|
1038
|
-
if (param.
|
|
1861
|
+
if (param.questionToken)
|
|
1039
1862
|
result.add(index);
|
|
1040
1863
|
});
|
|
1041
1864
|
return result;
|
|
1042
1865
|
}
|
|
1043
1866
|
function parseModifierParams(cls, modifierName) {
|
|
1044
1867
|
const result = new Set;
|
|
1045
|
-
const ctor = cls.
|
|
1868
|
+
const ctor = cls.members.find(ts3.isConstructorDeclaration);
|
|
1046
1869
|
if (!ctor)
|
|
1047
1870
|
return result;
|
|
1048
|
-
ctor.
|
|
1049
|
-
for (const dec of param
|
|
1050
|
-
if (
|
|
1871
|
+
ctor.parameters.forEach((param, index) => {
|
|
1872
|
+
for (const dec of decoratorsOf(param)) {
|
|
1873
|
+
if (decoratorName2(dec) === modifierName)
|
|
1051
1874
|
result.add(index);
|
|
1052
1875
|
}
|
|
1053
1876
|
});
|
|
1054
1877
|
return result;
|
|
1055
1878
|
}
|
|
1056
1879
|
function unwrapForwardRef(expr) {
|
|
1057
|
-
if (
|
|
1058
|
-
const exprText = expr.
|
|
1880
|
+
if (ts3.isCallExpression(expr)) {
|
|
1881
|
+
const exprText = nodeText(expr.expression);
|
|
1059
1882
|
if (exprText === "forwardRef" || exprText.endsWith(".forwardRef")) {
|
|
1060
|
-
const arg = expr.
|
|
1061
|
-
if (arg && (
|
|
1062
|
-
const body = arg.
|
|
1063
|
-
if (body &&
|
|
1883
|
+
const arg = expr.arguments[0];
|
|
1884
|
+
if (arg && (ts3.isArrowFunction(arg) || ts3.isFunctionExpression(arg))) {
|
|
1885
|
+
const body = arg.body;
|
|
1886
|
+
if (body && ts3.isExpression(body)) {
|
|
1064
1887
|
return unwrapForwardRef(body);
|
|
1065
1888
|
}
|
|
1066
1889
|
}
|
|
@@ -1068,18 +1891,18 @@ function unwrapForwardRef(expr) {
|
|
|
1068
1891
|
}
|
|
1069
1892
|
return expr;
|
|
1070
1893
|
}
|
|
1071
|
-
function tokenText(expr) {
|
|
1894
|
+
function tokenText(expr, ctx) {
|
|
1072
1895
|
const unwrapped = unwrapForwardRef(expr);
|
|
1073
|
-
if (
|
|
1074
|
-
return unwrapped.
|
|
1075
|
-
if (
|
|
1076
|
-
const decl = resolveDeclaration(unwrapped)[0];
|
|
1077
|
-
if (decl &&
|
|
1078
|
-
return decl.
|
|
1079
|
-
if (decl &&
|
|
1080
|
-
return decl
|
|
1896
|
+
if (ts3.isStringLiteral(unwrapped))
|
|
1897
|
+
return unwrapped.text;
|
|
1898
|
+
if (ts3.isIdentifier(unwrapped)) {
|
|
1899
|
+
const decl = resolveDeclaration(unwrapped, ctx)[0];
|
|
1900
|
+
if (decl && ts3.isClassDeclaration(decl))
|
|
1901
|
+
return decl.name?.text ?? unwrapped.text;
|
|
1902
|
+
if (decl && ts3.isVariableDeclaration(decl))
|
|
1903
|
+
return variableName(decl);
|
|
1081
1904
|
}
|
|
1082
|
-
return unwrapped
|
|
1905
|
+
return nodeText(unwrapped);
|
|
1083
1906
|
}
|
|
1084
1907
|
function resolveScope(input, ctx) {
|
|
1085
1908
|
if (input.explicit)
|
|
@@ -1096,98 +1919,181 @@ function resolveScope(input, ctx) {
|
|
|
1096
1919
|
}
|
|
1097
1920
|
function tokenNameOf(expr, ctx) {
|
|
1098
1921
|
const unwrapped = unwrapForwardRef(expr);
|
|
1099
|
-
if (
|
|
1100
|
-
const decl = resolveDeclaration(unwrapped)[0];
|
|
1101
|
-
if (decl &&
|
|
1102
|
-
return { name: decl.
|
|
1922
|
+
if (ts3.isIdentifier(unwrapped)) {
|
|
1923
|
+
const decl = resolveDeclaration(unwrapped, ctx)[0];
|
|
1924
|
+
if (decl && ts3.isClassDeclaration(decl)) {
|
|
1925
|
+
return { name: decl.name?.text ?? nodeText(expr), kind: "class" };
|
|
1103
1926
|
}
|
|
1104
|
-
if (decl &&
|
|
1105
|
-
const name = decl
|
|
1927
|
+
if (decl && ts3.isVariableDeclaration(decl)) {
|
|
1928
|
+
const name = variableName(decl);
|
|
1106
1929
|
return { name, kind: ctx.tokensByName.has(name) ? "injection-token" : "class" };
|
|
1107
1930
|
}
|
|
1108
|
-
if (ctx.tokensByName.has(
|
|
1109
|
-
return { name:
|
|
1931
|
+
if (ctx.tokensByName.has(unwrapped.text)) {
|
|
1932
|
+
return { name: unwrapped.text, kind: "injection-token" };
|
|
1110
1933
|
}
|
|
1111
1934
|
}
|
|
1112
|
-
return { name: expr
|
|
1935
|
+
return { name: nodeText(expr), kind: "class" };
|
|
1113
1936
|
}
|
|
1114
|
-
function resolveDeclaration(id) {
|
|
1115
|
-
let symbol =
|
|
1937
|
+
function resolveDeclaration(id, ctx) {
|
|
1938
|
+
let symbol = ctx.checker.getSymbolAtLocation(id);
|
|
1116
1939
|
if (!symbol)
|
|
1117
1940
|
return [];
|
|
1118
|
-
let declarations = symbol.
|
|
1941
|
+
let declarations = symbol.declarations ?? [];
|
|
1119
1942
|
for (let guard = 0;guard < 4; guard += 1) {
|
|
1120
|
-
const isAlias = declarations.some((d) =>
|
|
1943
|
+
const isAlias = declarations.some((d) => ts3.isImportSpecifier(d) || ts3.isImportClause(d) || ts3.isNamespaceImport(d));
|
|
1121
1944
|
if (!isAlias)
|
|
1122
1945
|
break;
|
|
1123
|
-
|
|
1124
|
-
if (!aliased)
|
|
1946
|
+
if (!(symbol.flags & ts3.SymbolFlags.Alias))
|
|
1125
1947
|
break;
|
|
1948
|
+
const aliased = ctx.checker.getAliasedSymbol(symbol);
|
|
1126
1949
|
symbol = aliased;
|
|
1127
|
-
declarations = aliased.
|
|
1950
|
+
declarations = aliased.declarations ?? [];
|
|
1128
1951
|
}
|
|
1129
1952
|
return declarations;
|
|
1130
1953
|
}
|
|
1131
1954
|
function importPathOf(id, ctx) {
|
|
1132
|
-
const
|
|
1133
|
-
const first = symbol?.getDeclarations()[0];
|
|
1134
|
-
if (first && (Node.isImportSpecifier(first) || Node.isImportClause(first))) {
|
|
1135
|
-
const importDecl = first.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
|
|
1136
|
-
const target = importDecl?.getModuleSpecifierSourceFile();
|
|
1137
|
-
if (target)
|
|
1138
|
-
return modulePath(ctx.rootDir, target.getFilePath());
|
|
1139
|
-
}
|
|
1140
|
-
const decl = resolveDeclaration(id)[0];
|
|
1955
|
+
const decl = resolveDeclaration(id, ctx)[0];
|
|
1141
1956
|
if (decl)
|
|
1142
|
-
return modulePath(ctx.rootDir, decl.getSourceFile().
|
|
1957
|
+
return modulePath(ctx.rootDir, decl.getSourceFile().fileName);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
function importModuleOf(id, ctx) {
|
|
1961
|
+
const symbol = ctx.checker.getSymbolAtLocation(id);
|
|
1962
|
+
const declarations = symbol?.declarations ?? [];
|
|
1963
|
+
for (const declaration of declarations) {
|
|
1964
|
+
let current = declaration;
|
|
1965
|
+
while (current) {
|
|
1966
|
+
if (ts3.isImportDeclaration(current)) {
|
|
1967
|
+
const moduleSpecifier = current.moduleSpecifier;
|
|
1968
|
+
return ts3.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : undefined;
|
|
1969
|
+
}
|
|
1970
|
+
current = current.parent;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1143
1973
|
return;
|
|
1144
1974
|
}
|
|
1145
1975
|
function findDecorator(cls, name) {
|
|
1146
|
-
return cls
|
|
1976
|
+
return decoratorsOf(cls).find((dec) => decoratorName2(dec) === name);
|
|
1147
1977
|
}
|
|
1148
|
-
function
|
|
1149
|
-
const expr = dec.
|
|
1150
|
-
if (
|
|
1151
|
-
return expr.
|
|
1978
|
+
function decoratorName2(dec) {
|
|
1979
|
+
const expr = dec.expression;
|
|
1980
|
+
if (ts3.isCallExpression(expr)) {
|
|
1981
|
+
return nodeText(expr.expression).split(".").pop();
|
|
1152
1982
|
}
|
|
1153
|
-
if (
|
|
1154
|
-
return expr.
|
|
1983
|
+
if (ts3.isIdentifier(expr))
|
|
1984
|
+
return expr.text;
|
|
1155
1985
|
return;
|
|
1156
1986
|
}
|
|
1157
1987
|
function decoratorObjectArg(dec) {
|
|
1158
|
-
const expr = dec.
|
|
1159
|
-
if (!
|
|
1988
|
+
const expr = dec.expression;
|
|
1989
|
+
if (!ts3.isCallExpression(expr))
|
|
1160
1990
|
return;
|
|
1161
|
-
const arg = expr.
|
|
1162
|
-
return arg &&
|
|
1991
|
+
const arg = expr.arguments[0];
|
|
1992
|
+
return arg && ts3.isObjectLiteralExpression(arg) ? arg : undefined;
|
|
1163
1993
|
}
|
|
1164
1994
|
function getProp(obj, name) {
|
|
1165
|
-
const prop = obj.
|
|
1166
|
-
if (prop
|
|
1167
|
-
return
|
|
1995
|
+
const prop = obj.properties.find((item) => (ts3.isPropertyAssignment(item) || ts3.isShorthandPropertyAssignment(item)) && propertyName(item.name) === name);
|
|
1996
|
+
if (!prop)
|
|
1997
|
+
return;
|
|
1998
|
+
if (ts3.isPropertyAssignment(prop))
|
|
1999
|
+
return prop.initializer;
|
|
2000
|
+
if (ts3.isShorthandPropertyAssignment(prop))
|
|
2001
|
+
return prop.name;
|
|
1168
2002
|
return;
|
|
1169
2003
|
}
|
|
2004
|
+
function toCompilerDiagnostic(diagnostic, rootDir) {
|
|
2005
|
+
const file = diagnostic.file;
|
|
2006
|
+
const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : undefined;
|
|
2007
|
+
return {
|
|
2008
|
+
severity: diagnostic.category === ts3.DiagnosticCategory.Error ? "error" : "warn",
|
|
2009
|
+
code: `typescript-${diagnostic.code}`,
|
|
2010
|
+
errorCode: `TS${diagnostic.code}`,
|
|
2011
|
+
message: ts3.flattenDiagnosticMessageText(diagnostic.messageText, `
|
|
2012
|
+
`),
|
|
2013
|
+
file: file ? sourcePath(rootDir, file.fileName) : undefined,
|
|
2014
|
+
line: position ? position.line + 1 : undefined
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
1170
2017
|
function stringLiteralProp(obj, name) {
|
|
1171
2018
|
const expr = getProp(obj, name);
|
|
1172
|
-
return expr &&
|
|
2019
|
+
return expr && ts3.isStringLiteral(expr) ? expr.text : undefined;
|
|
1173
2020
|
}
|
|
1174
2021
|
function arrayProp(obj, name) {
|
|
1175
2022
|
const expr = getProp(obj, name);
|
|
1176
|
-
return expr &&
|
|
2023
|
+
return expr && ts3.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
2024
|
+
}
|
|
2025
|
+
function parseAspectRefs(expression, ctx, owner) {
|
|
2026
|
+
if (!expression)
|
|
2027
|
+
return [];
|
|
2028
|
+
if (!ts3.isArrayLiteralExpression(expression)) {
|
|
2029
|
+
ctx.diagnostics.push({
|
|
2030
|
+
severity: "error",
|
|
2031
|
+
code: "dynamic-aspect-reference",
|
|
2032
|
+
message: `${owner} 的 aspects 必须是显式数组字面量,并且每一项必须是可解析的函数引用`,
|
|
2033
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
2034
|
+
line: lineOf(expression),
|
|
2035
|
+
suggestion: "使用 aspects: [auditAspect, transactionAspect],不要使用变量、调用表达式或字符串 pointcut。",
|
|
2036
|
+
errorCode: "SC4010",
|
|
2037
|
+
docsUrl: "https://supacloud.dev/errors/SC4010"
|
|
2038
|
+
});
|
|
2039
|
+
return [];
|
|
2040
|
+
}
|
|
2041
|
+
const refs = [];
|
|
2042
|
+
for (const element of expression.elements) {
|
|
2043
|
+
if (ts3.isSpreadElement(element) || !ts3.isIdentifier(element)) {
|
|
2044
|
+
ctx.diagnostics.push({
|
|
2045
|
+
severity: "error",
|
|
2046
|
+
code: "dynamic-aspect-reference",
|
|
2047
|
+
message: `${owner} 的 aspects 只能包含显式的函数标识符引用,无法静态编译 '${nodeText(element)}'`,
|
|
2048
|
+
file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
|
|
2049
|
+
line: lineOf(element),
|
|
2050
|
+
suggestion: "将 aspect 直接写入数组,例如 aspects: [auditAspect]。",
|
|
2051
|
+
errorCode: "SC4010",
|
|
2052
|
+
docsUrl: "https://supacloud.dev/errors/SC4010"
|
|
2053
|
+
});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
const declaration = resolveDeclaration(element, ctx).find((candidate) => ts3.isFunctionDeclaration(candidate) || ts3.isVariableDeclaration(candidate) && candidate.initializer !== undefined && (ts3.isArrowFunction(candidate.initializer) || ts3.isFunctionExpression(candidate.initializer)));
|
|
2057
|
+
if (!declaration) {
|
|
2058
|
+
ctx.diagnostics.push({
|
|
2059
|
+
severity: "error",
|
|
2060
|
+
code: "invalid-aspect-reference",
|
|
2061
|
+
message: `${owner} 引用了 '${element.text}',但它不是可静态解析的 aspect 函数`,
|
|
2062
|
+
file: sourcePath(ctx.rootDir, element.getSourceFile().fileName),
|
|
2063
|
+
line: lineOf(element),
|
|
2064
|
+
suggestion: "aspect 必须是函数声明、箭头函数或函数表达式的直接引用。",
|
|
2065
|
+
errorCode: "SC4011",
|
|
2066
|
+
docsUrl: "https://supacloud.dev/errors/SC4011"
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
const name = ts3.isFunctionDeclaration(declaration) ? declaration.name?.text : ts3.isVariableDeclaration(declaration) ? variableName(declaration) : undefined;
|
|
2071
|
+
if (!name)
|
|
2072
|
+
continue;
|
|
2073
|
+
const declaredFile = declaration.getSourceFile().fileName;
|
|
2074
|
+
const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
|
|
2075
|
+
refs.push({
|
|
2076
|
+
name,
|
|
2077
|
+
expression: element.text,
|
|
2078
|
+
importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
|
|
2079
|
+
importModule: projectLocal ? undefined : importModuleOf(element, ctx)
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
return refs;
|
|
1177
2083
|
}
|
|
1178
2084
|
function booleanProp(obj, name) {
|
|
1179
2085
|
const expr = getProp(obj, name);
|
|
1180
2086
|
if (!expr)
|
|
1181
2087
|
return;
|
|
1182
|
-
if (expr.
|
|
2088
|
+
if (expr.kind === ts3.SyntaxKind.TrueKeyword)
|
|
1183
2089
|
return true;
|
|
1184
|
-
if (expr.
|
|
2090
|
+
if (expr.kind === ts3.SyntaxKind.FalseKeyword)
|
|
1185
2091
|
return false;
|
|
1186
2092
|
return;
|
|
1187
2093
|
}
|
|
1188
2094
|
function parseScopeProp(obj) {
|
|
1189
2095
|
const scope = stringLiteralProp(obj, "scope");
|
|
1190
|
-
return scope &&
|
|
2096
|
+
return scope && isScope(scope) ? scope : undefined;
|
|
1191
2097
|
}
|
|
1192
2098
|
function parseBindingOptions(args, defaultName) {
|
|
1193
2099
|
let name = defaultName;
|
|
@@ -1195,16 +2101,16 @@ function parseBindingOptions(args, defaultName) {
|
|
|
1195
2101
|
let defaultValue;
|
|
1196
2102
|
const first = args[0];
|
|
1197
2103
|
const second = args[1];
|
|
1198
|
-
if (first &&
|
|
1199
|
-
name = first.
|
|
1200
|
-
} else if (first &&
|
|
2104
|
+
if (first && ts3.isStringLiteral(first)) {
|
|
2105
|
+
name = first.text;
|
|
2106
|
+
} else if (first && ts3.isObjectLiteralExpression(first)) {
|
|
1201
2107
|
const nameProp = getProp(first, "name");
|
|
1202
|
-
if (nameProp &&
|
|
1203
|
-
name = nameProp.
|
|
2108
|
+
if (nameProp && ts3.isStringLiteral(nameProp)) {
|
|
2109
|
+
name = nameProp.text;
|
|
1204
2110
|
}
|
|
1205
2111
|
const trProp = getProp(first, "transform");
|
|
1206
|
-
if (trProp &&
|
|
1207
|
-
const val = trProp.
|
|
2112
|
+
if (trProp && ts3.isStringLiteral(trProp)) {
|
|
2113
|
+
const val = trProp.text;
|
|
1208
2114
|
if (val === "number" || val === "boolean" || val === "string") {
|
|
1209
2115
|
transform = val;
|
|
1210
2116
|
}
|
|
@@ -1214,10 +2120,10 @@ function parseBindingOptions(args, defaultName) {
|
|
|
1214
2120
|
defaultValue = parseLiteralValue(defProp);
|
|
1215
2121
|
}
|
|
1216
2122
|
}
|
|
1217
|
-
if (second &&
|
|
2123
|
+
if (second && ts3.isObjectLiteralExpression(second)) {
|
|
1218
2124
|
const trProp = getProp(second, "transform");
|
|
1219
|
-
if (trProp &&
|
|
1220
|
-
const val = trProp.
|
|
2125
|
+
if (trProp && ts3.isStringLiteral(trProp)) {
|
|
2126
|
+
const val = trProp.text;
|
|
1221
2127
|
if (val === "number" || val === "boolean" || val === "string") {
|
|
1222
2128
|
transform = val;
|
|
1223
2129
|
}
|
|
@@ -1230,28 +2136,28 @@ function parseBindingOptions(args, defaultName) {
|
|
|
1230
2136
|
return { name, transform, default: defaultValue };
|
|
1231
2137
|
}
|
|
1232
2138
|
function parseLiteralValue(node) {
|
|
1233
|
-
if (
|
|
1234
|
-
return node.
|
|
1235
|
-
if (
|
|
1236
|
-
return node.
|
|
1237
|
-
if (node.
|
|
2139
|
+
if (ts3.isStringLiteral(node))
|
|
2140
|
+
return node.text;
|
|
2141
|
+
if (ts3.isNumericLiteral(node))
|
|
2142
|
+
return Number(node.text);
|
|
2143
|
+
if (node.kind === ts3.SyntaxKind.TrueKeyword)
|
|
1238
2144
|
return true;
|
|
1239
|
-
if (node.
|
|
2145
|
+
if (node.kind === ts3.SyntaxKind.FalseKeyword)
|
|
1240
2146
|
return false;
|
|
1241
|
-
if (
|
|
1242
|
-
return node.
|
|
2147
|
+
if (ts3.isArrayLiteralExpression(node)) {
|
|
2148
|
+
return node.elements.map(parseLiteralValue);
|
|
1243
2149
|
}
|
|
1244
|
-
if (
|
|
2150
|
+
if (ts3.isObjectLiteralExpression(node)) {
|
|
1245
2151
|
return parseObjectLiteralValues(node);
|
|
1246
2152
|
}
|
|
1247
2153
|
return;
|
|
1248
2154
|
}
|
|
1249
2155
|
function parseObjectLiteralValues(obj) {
|
|
1250
2156
|
const result = {};
|
|
1251
|
-
for (const prop of obj.
|
|
1252
|
-
if (
|
|
1253
|
-
const name = prop.
|
|
1254
|
-
const init = prop.
|
|
2157
|
+
for (const prop of obj.properties) {
|
|
2158
|
+
if (ts3.isPropertyAssignment(prop)) {
|
|
2159
|
+
const name = propertyName(prop.name);
|
|
2160
|
+
const init = prop.initializer;
|
|
1255
2161
|
if (init) {
|
|
1256
2162
|
result[name] = parseLiteralValue(init);
|
|
1257
2163
|
}
|
|
@@ -1269,63 +2175,9 @@ function warn(ctx, code, message, file, line) {
|
|
|
1269
2175
|
ctx.diagnostics.push({ severity: "warn", code, message, file, line });
|
|
1270
2176
|
}
|
|
1271
2177
|
// src/generate.ts
|
|
1272
|
-
import {
|
|
2178
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2179
|
+
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
1273
2180
|
import { join as join2 } from "node:path";
|
|
1274
|
-
|
|
1275
|
-
// src/util.ts
|
|
1276
|
-
function camelName(token) {
|
|
1277
|
-
const isConstantCase = token.includes("_") || !/[a-z]/.test(token);
|
|
1278
|
-
if (isConstantCase) {
|
|
1279
|
-
return token.toLowerCase().split("_").filter((part) => part.length > 0).map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1280
|
-
}
|
|
1281
|
-
return token.charAt(0).toLowerCase() + token.slice(1);
|
|
1282
|
-
}
|
|
1283
|
-
function relativeImportPath(fromDir, toFile) {
|
|
1284
|
-
const fromParts = fromDir.split("/").filter(Boolean);
|
|
1285
|
-
const toParts = toFile.split("/").filter(Boolean);
|
|
1286
|
-
let common = 0;
|
|
1287
|
-
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
1288
|
-
common += 1;
|
|
1289
|
-
}
|
|
1290
|
-
const ups = fromParts.length - common;
|
|
1291
|
-
const downs = toParts.slice(common);
|
|
1292
|
-
const last = downs[downs.length - 1]?.replace(/\.(ts|tsx|js|mts|cts)$/, "") ?? "";
|
|
1293
|
-
const segments = [...Array(ups).fill(".."), ...downs.slice(0, -1), last];
|
|
1294
|
-
const joined = segments.join("/");
|
|
1295
|
-
return joined.startsWith("..") ? joined : `./${joined}`;
|
|
1296
|
-
}
|
|
1297
|
-
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context";
|
|
1298
|
-
var JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
1299
|
-
function isRequestContextToken(token, tokenNames) {
|
|
1300
|
-
return token === "REQUEST_CONTEXT" || tokenNames?.[token] === REQUEST_CONTEXT_TOKEN_NAME;
|
|
1301
|
-
}
|
|
1302
|
-
function isJobContextToken(token, tokenNames) {
|
|
1303
|
-
return token === "JOB_CONTEXT" || tokenNames?.[token] === JOB_CONTEXT_TOKEN_NAME;
|
|
1304
|
-
}
|
|
1305
|
-
function joinRoutePaths(prefix, path) {
|
|
1306
|
-
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1307
|
-
const normalized = joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
1308
|
-
return normalized;
|
|
1309
|
-
}
|
|
1310
|
-
function findClosestMatch(target, candidates) {
|
|
1311
|
-
if (candidates.length === 0)
|
|
1312
|
-
return;
|
|
1313
|
-
if (candidates.length === 1)
|
|
1314
|
-
return candidates[0];
|
|
1315
|
-
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1316
|
-
const targetNorm = norm(target);
|
|
1317
|
-
for (const c of candidates) {
|
|
1318
|
-
if (norm(c) === targetNorm)
|
|
1319
|
-
return c;
|
|
1320
|
-
}
|
|
1321
|
-
for (const c of candidates) {
|
|
1322
|
-
if (norm(c).includes(targetNorm) || targetNorm.includes(norm(c)))
|
|
1323
|
-
return c;
|
|
1324
|
-
}
|
|
1325
|
-
return candidates[0];
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// src/generate.ts
|
|
1329
2181
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
1330
2182
|
var INTERFACES = `export interface CompiledRoute {
|
|
1331
2183
|
method: string;
|
|
@@ -1348,6 +2200,7 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
1348
2200
|
queryDefaults?: Record<string, unknown>;
|
|
1349
2201
|
title?: string;
|
|
1350
2202
|
data?: Record<string, unknown>;
|
|
2203
|
+
aspects?: CompiledAspect[];
|
|
1351
2204
|
invoker?: (
|
|
1352
2205
|
controller: unknown,
|
|
1353
2206
|
request: {
|
|
@@ -1368,8 +2221,33 @@ export interface CompiledCommand {
|
|
|
1368
2221
|
audit?: string;
|
|
1369
2222
|
idempotency: "required" | "none";
|
|
1370
2223
|
standalone?: boolean;
|
|
2224
|
+
aspects?: CompiledAspect[];
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
export interface CompiledJob {
|
|
2228
|
+
className: string;
|
|
2229
|
+
name: string;
|
|
2230
|
+
serviceKey: string;
|
|
2231
|
+
scope: "application" | "request" | "job";
|
|
2232
|
+
aspects?: CompiledAspect[];
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
export interface CompiledAspectContext {
|
|
2236
|
+
kind: "route" | "command" | "job";
|
|
2237
|
+
name: string;
|
|
2238
|
+
input: unknown;
|
|
2239
|
+
request?: Request;
|
|
2240
|
+
requestContext?: unknown;
|
|
2241
|
+
scope?: Record<string, unknown>;
|
|
2242
|
+
services?: Record<string, unknown>;
|
|
2243
|
+
metadata?: unknown;
|
|
1371
2244
|
}
|
|
1372
2245
|
|
|
2246
|
+
export type CompiledAspect = (
|
|
2247
|
+
context: CompiledAspectContext,
|
|
2248
|
+
next: () => unknown | Promise<unknown>,
|
|
2249
|
+
) => unknown | Promise<unknown>;
|
|
2250
|
+
|
|
1373
2251
|
export interface CompiledController {
|
|
1374
2252
|
path: string;
|
|
1375
2253
|
serviceKey: string;
|
|
@@ -1387,14 +2265,63 @@ export interface CompiledModule {
|
|
|
1387
2265
|
services: Record<string, unknown>,
|
|
1388
2266
|
ctx: unknown,
|
|
1389
2267
|
imported?: Record<string, Record<string, unknown>>,
|
|
1390
|
-
): Record<string, unknown
|
|
2268
|
+
): Promise<Record<string, unknown>>;
|
|
2269
|
+
destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
|
|
1391
2270
|
createJobScope?(
|
|
1392
2271
|
services: Record<string, unknown>,
|
|
1393
2272
|
ctx: unknown,
|
|
1394
2273
|
imported?: Record<string, Record<string, unknown>>,
|
|
1395
|
-
): Record<string, unknown
|
|
2274
|
+
): Promise<Record<string, unknown>>;
|
|
2275
|
+
destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
|
|
1396
2276
|
controllers: CompiledController[];
|
|
1397
2277
|
commands: CompiledCommand[];
|
|
2278
|
+
jobs: CompiledJob[];
|
|
2279
|
+
aspects?: CompiledAspect[];
|
|
2280
|
+
}`;
|
|
2281
|
+
var TYPE_GUARDS = `function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2282
|
+
return typeof value === "object" && value !== null;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
|
|
2286
|
+
return typeof value === "function";
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function resolveFactoryValue(value: unknown): unknown {
|
|
2290
|
+
if (!isRecord(value) || !isFunction(value.factory)) return undefined;
|
|
2291
|
+
return value.factory();
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
const scopeDestructions = new WeakMap<object, Promise<void>>();
|
|
2295
|
+
|
|
2296
|
+
function destroyScopeInstances(
|
|
2297
|
+
scope: Record<string, unknown>,
|
|
2298
|
+
plan: readonly { key: string; index?: number }[],
|
|
2299
|
+
): Promise<void> {
|
|
2300
|
+
const pending = scopeDestructions.get(scope);
|
|
2301
|
+
if (pending) return pending;
|
|
2302
|
+
const destruction = Promise.resolve().then(async () => {
|
|
2303
|
+
const errors: unknown[] = [];
|
|
2304
|
+
const seen = new Set<unknown>();
|
|
2305
|
+
for (const entry of [...plan].reverse()) {
|
|
2306
|
+
const value = scope[entry.key];
|
|
2307
|
+
const instance = entry.index === undefined ? value
|
|
2308
|
+
: Array.isArray(value) ? value[entry.index] : undefined;
|
|
2309
|
+
if (seen.has(instance)) continue;
|
|
2310
|
+
seen.add(instance);
|
|
2311
|
+
try {
|
|
2312
|
+
if (isRecord(instance) && isFunction(instance.onDestroy)) {
|
|
2313
|
+
await instance.onDestroy();
|
|
2314
|
+
} else if (isRecord(instance) && isFunction(instance.ngOnDestroy)) {
|
|
2315
|
+
await instance.ngOnDestroy();
|
|
2316
|
+
}
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
errors.push(error);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (errors.length > 0) throw new AggregateError(errors, "Scope destruction failed");
|
|
2322
|
+
});
|
|
2323
|
+
scopeDestructions.set(scope, destruction);
|
|
2324
|
+
return destruction;
|
|
1398
2325
|
}`;
|
|
1399
2326
|
function renderApplication(graph, options) {
|
|
1400
2327
|
let modules = topoSortModules(graph.modules);
|
|
@@ -1412,6 +2339,8 @@ function renderApplication(graph, options) {
|
|
|
1412
2339
|
referencedTokens.add(d);
|
|
1413
2340
|
for (const d of ctrl.skipSelfDeps ?? [])
|
|
1414
2341
|
referencedTokens.add(d);
|
|
2342
|
+
for (const d of ctrl.hostDeps ?? [])
|
|
2343
|
+
referencedTokens.add(d);
|
|
1415
2344
|
}
|
|
1416
2345
|
for (const p of mod.providers) {
|
|
1417
2346
|
for (const d of p.deps ?? [])
|
|
@@ -1422,6 +2351,8 @@ function renderApplication(graph, options) {
|
|
|
1422
2351
|
referencedTokens.add(d);
|
|
1423
2352
|
for (const d of p.skipSelfDeps ?? [])
|
|
1424
2353
|
referencedTokens.add(d);
|
|
2354
|
+
for (const d of p.hostDeps ?? [])
|
|
2355
|
+
referencedTokens.add(d);
|
|
1425
2356
|
if (p.useExisting)
|
|
1426
2357
|
referencedTokens.add(p.useExisting);
|
|
1427
2358
|
}
|
|
@@ -1446,6 +2377,8 @@ function renderApplication(graph, options) {
|
|
|
1446
2377
|
...imports.size > 0 ? [""] : [],
|
|
1447
2378
|
INTERFACES,
|
|
1448
2379
|
"",
|
|
2380
|
+
TYPE_GUARDS,
|
|
2381
|
+
"",
|
|
1449
2382
|
"export function createCompiledModules(): CompiledModule[] {",
|
|
1450
2383
|
" return [",
|
|
1451
2384
|
...descriptorEntries.map((entry) => indent(entry, 4) + ","),
|
|
@@ -1453,29 +2386,33 @@ function renderApplication(graph, options) {
|
|
|
1453
2386
|
"}",
|
|
1454
2387
|
"",
|
|
1455
2388
|
"export async function initializeApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1456
|
-
' const initializers =
|
|
1457
|
-
"
|
|
1458
|
-
"
|
|
1459
|
-
|
|
2389
|
+
' const initializers = [services.environmentInitializer ?? services["supacloud.environment-initializer"], services.appInitializer ?? services["supacloud.app-initializer"]];',
|
|
2390
|
+
" for (const group of initializers) {",
|
|
2391
|
+
" if (Array.isArray(group)) {",
|
|
2392
|
+
" for (const init of group) {",
|
|
2393
|
+
" if (isFunction(init)) await init();",
|
|
2394
|
+
" }",
|
|
2395
|
+
" } else if (isFunction(group)) {",
|
|
2396
|
+
" await group();",
|
|
1460
2397
|
" }",
|
|
1461
|
-
' } else if (typeof initializers === "function") {',
|
|
1462
|
-
" await (initializers as () => unknown)();",
|
|
1463
2398
|
" }",
|
|
1464
2399
|
"}",
|
|
1465
2400
|
"",
|
|
1466
2401
|
"export async function destroyApplication(services: Record<string, unknown>): Promise<void> {",
|
|
1467
|
-
' const destroyRef =
|
|
1468
|
-
|
|
2402
|
+
' const destroyRef = services.destroyRef ?? services["supacloud.destroy-ref"];',
|
|
2403
|
+
" if (isRecord(destroyRef) && isFunction(destroyRef.destroy)) {",
|
|
1469
2404
|
" await destroyRef.destroy();",
|
|
1470
|
-
" } else if (destroyRef && Array.isArray(destroyRef._teardowns)) {",
|
|
2405
|
+
" } else if (isRecord(destroyRef) && Array.isArray(destroyRef._teardowns)) {",
|
|
1471
2406
|
" for (const teardown of [...destroyRef._teardowns].reverse()) {",
|
|
1472
|
-
|
|
2407
|
+
" if (isFunction(teardown)) await teardown();",
|
|
1473
2408
|
" }",
|
|
1474
2409
|
" }",
|
|
1475
2410
|
" const instances = Object.values(services);",
|
|
1476
2411
|
" for (const inst of instances.reverse()) {",
|
|
1477
|
-
|
|
1478
|
-
" await
|
|
2412
|
+
" if (isRecord(inst) && isFunction(inst.onDestroy)) {",
|
|
2413
|
+
" await inst.onDestroy();",
|
|
2414
|
+
" } else if (isRecord(inst) && isFunction(inst.ngOnDestroy)) {",
|
|
2415
|
+
" await inst.ngOnDestroy();",
|
|
1479
2416
|
" }",
|
|
1480
2417
|
" }",
|
|
1481
2418
|
"}",
|
|
@@ -1504,21 +2441,39 @@ async function generateApplication(graph, options) {
|
|
|
1504
2441
|
await mkdir(options.outDir, { recursive: true });
|
|
1505
2442
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
1506
2443
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
1507
|
-
|
|
1508
|
-
await
|
|
1509
|
-
|
|
2444
|
+
const written = [];
|
|
2445
|
+
if (await writeFileIfChanged(applicationPath, rendered.applicationCode, options.artifactHashes)) {
|
|
2446
|
+
written.push(applicationPath);
|
|
2447
|
+
}
|
|
2448
|
+
if (await writeFileIfChanged(manifestPath, rendered.manifestJson, options.artifactHashes)) {
|
|
2449
|
+
written.push(manifestPath);
|
|
2450
|
+
}
|
|
1510
2451
|
if (rendered.clientCode) {
|
|
1511
2452
|
const clientPath = join2(options.outDir, "client.ts");
|
|
1512
|
-
await
|
|
1513
|
-
|
|
2453
|
+
if (await writeFileIfChanged(clientPath, rendered.clientCode, options.artifactHashes)) {
|
|
2454
|
+
written.push(clientPath);
|
|
2455
|
+
}
|
|
1514
2456
|
}
|
|
1515
2457
|
if (rendered.permissionsCode) {
|
|
1516
2458
|
const permissionsPath = join2(options.outDir, "permissions.ts");
|
|
1517
|
-
await
|
|
1518
|
-
|
|
2459
|
+
if (await writeFileIfChanged(permissionsPath, rendered.permissionsCode, options.artifactHashes)) {
|
|
2460
|
+
written.push(permissionsPath);
|
|
2461
|
+
}
|
|
1519
2462
|
}
|
|
1520
2463
|
return written;
|
|
1521
2464
|
}
|
|
2465
|
+
async function writeFileIfChanged(path, content, hashes) {
|
|
2466
|
+
const hash = createHash4("sha1").update(content).digest("hex");
|
|
2467
|
+
if (hashes?.get(path) === hash) {
|
|
2468
|
+
try {
|
|
2469
|
+
await access(path);
|
|
2470
|
+
return false;
|
|
2471
|
+
} catch {}
|
|
2472
|
+
}
|
|
2473
|
+
await writeFileAtomic(path, content);
|
|
2474
|
+
hashes?.set(path, hash);
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
1522
2477
|
async function writeFileAtomic(path, content) {
|
|
1523
2478
|
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1524
2479
|
try {
|
|
@@ -1569,11 +2524,13 @@ class ImportManager {
|
|
|
1569
2524
|
get size() {
|
|
1570
2525
|
return this.entries.size;
|
|
1571
2526
|
}
|
|
1572
|
-
add(exported, importPath) {
|
|
1573
|
-
|
|
2527
|
+
add(exported, importPath, importModule) {
|
|
2528
|
+
const path = importModule ?? importPath;
|
|
2529
|
+
const packageImport = importModule !== undefined;
|
|
2530
|
+
if (!path)
|
|
1574
2531
|
return exported;
|
|
1575
2532
|
for (const [local2, entry] of this.entries) {
|
|
1576
|
-
if (entry.path ===
|
|
2533
|
+
if (entry.path === path && entry.exported === exported && entry.package === packageImport)
|
|
1577
2534
|
return local2;
|
|
1578
2535
|
}
|
|
1579
2536
|
let local = exported;
|
|
@@ -1582,18 +2539,18 @@ class ImportManager {
|
|
|
1582
2539
|
local = `${exported}${counter}`;
|
|
1583
2540
|
counter += 1;
|
|
1584
2541
|
}
|
|
1585
|
-
this.entries.set(local, { path
|
|
2542
|
+
this.entries.set(local, { path, exported, package: packageImport });
|
|
1586
2543
|
return local;
|
|
1587
2544
|
}
|
|
1588
2545
|
render(rootDir, outDir) {
|
|
1589
2546
|
const byPath = new Map;
|
|
1590
2547
|
for (const [local, entry] of this.entries) {
|
|
1591
|
-
const
|
|
2548
|
+
const spec = entry.package ? entry.path : relativeImportPath(outDir, join2(rootDir, `${entry.path}.ts`));
|
|
2549
|
+
const list = byPath.get(spec) ?? [];
|
|
1592
2550
|
list.push({ exported: entry.exported, local });
|
|
1593
|
-
byPath.set(
|
|
2551
|
+
byPath.set(spec, list);
|
|
1594
2552
|
}
|
|
1595
|
-
return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([
|
|
1596
|
-
const spec = relativeImportPath(outDir, join2(rootDir, `${path}.ts`));
|
|
2553
|
+
return [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([spec, symbols]) => {
|
|
1597
2554
|
const names = symbols.sort((a, b) => a.exported.localeCompare(b.exported)).map((s) => s.local === s.exported ? s.exported : `${s.exported} as ${s.local}`).join(", ");
|
|
1598
2555
|
return `import { ${names} } from "${spec}";`;
|
|
1599
2556
|
});
|
|
@@ -1615,14 +2572,19 @@ class ModuleGenerator {
|
|
|
1615
2572
|
this.module = module;
|
|
1616
2573
|
this.imports = imports;
|
|
1617
2574
|
this.pascal = pascalName(module.name);
|
|
2575
|
+
if (module.providers.some((provider) => (provider.functionalInjects?.length ?? 0) > 0) || module.controllers.some((controller) => (controller.functionalInjects?.length ?? 0) > 0)) {
|
|
2576
|
+
imports.add("runInInjectionContext", undefined, "@supacloud/app");
|
|
2577
|
+
}
|
|
1618
2578
|
}
|
|
1619
2579
|
renderFactories() {
|
|
1620
2580
|
const sections = [this.renderServicesFactory()];
|
|
1621
2581
|
if (this.hasFactoryContent("request")) {
|
|
1622
2582
|
sections.push(this.renderScopeFactory("request"));
|
|
2583
|
+
sections.push(this.renderScopeDestroyer("request"));
|
|
1623
2584
|
}
|
|
1624
2585
|
if (this.hasFactoryContent("job")) {
|
|
1625
2586
|
sections.push(this.renderScopeFactory("job"));
|
|
2587
|
+
sections.push(this.renderScopeDestroyer("job"));
|
|
1626
2588
|
}
|
|
1627
2589
|
return sections;
|
|
1628
2590
|
}
|
|
@@ -1634,12 +2596,18 @@ class ModuleGenerator {
|
|
|
1634
2596
|
];
|
|
1635
2597
|
if (this.hasFactoryContent("request")) {
|
|
1636
2598
|
lines.push(` createRequestScope: create${this.pascal}RequestScope,`);
|
|
2599
|
+
lines.push(` destroyRequestScope: destroy${this.pascal}RequestScope,`);
|
|
1637
2600
|
}
|
|
1638
2601
|
if (this.hasFactoryContent("job")) {
|
|
1639
2602
|
lines.push(` createJobScope: create${this.pascal}JobScope,`);
|
|
2603
|
+
lines.push(` destroyJobScope: destroy${this.pascal}JobScope,`);
|
|
1640
2604
|
}
|
|
1641
2605
|
lines.push(` controllers: ${this.renderControllers()},`);
|
|
1642
|
-
lines.push(` commands: ${
|
|
2606
|
+
lines.push(` commands: ${this.renderCommands()},`);
|
|
2607
|
+
lines.push(` jobs: ${this.renderJobs()},`);
|
|
2608
|
+
if (this.module.aspects && this.module.aspects.length > 0) {
|
|
2609
|
+
lines.push(` aspects: ${this.renderAspects(this.module.aspects)},`);
|
|
2610
|
+
}
|
|
1643
2611
|
lines.push(`}`);
|
|
1644
2612
|
return lines.join(`
|
|
1645
2613
|
`);
|
|
@@ -1702,6 +2670,9 @@ class ModuleGenerator {
|
|
|
1702
2670
|
if (route.data && Object.keys(route.data).length > 0) {
|
|
1703
2671
|
fields.push(`data: ${JSON.stringify(route.data)}`);
|
|
1704
2672
|
}
|
|
2673
|
+
if (route.aspects && route.aspects.length > 0) {
|
|
2674
|
+
fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
|
|
2675
|
+
}
|
|
1705
2676
|
const invokerArgs = (route.handlerParams ?? []).map((hp) => {
|
|
1706
2677
|
if (hp.kind === "param") {
|
|
1707
2678
|
const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
|
|
@@ -1740,7 +2711,7 @@ class ModuleGenerator {
|
|
|
1740
2711
|
return "undefined";
|
|
1741
2712
|
});
|
|
1742
2713
|
const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
|
|
1743
|
-
fields.push(`invoker: async (ctrl:
|
|
2714
|
+
fields.push(`invoker: async (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (typeof handler !== "function") throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return await Reflect.apply(handler, ctrl, [${callArgs}]); }`);
|
|
1744
2715
|
return `{ ${fields.join(", ")} }`;
|
|
1745
2716
|
});
|
|
1746
2717
|
return [
|
|
@@ -1757,6 +2728,32 @@ class ModuleGenerator {
|
|
|
1757
2728
|
${indent(item, 2)}`).join(",")}
|
|
1758
2729
|
]`;
|
|
1759
2730
|
}
|
|
2731
|
+
renderCommands() {
|
|
2732
|
+
if (this.module.commands.length === 0)
|
|
2733
|
+
return "[]";
|
|
2734
|
+
return `[${this.module.commands.map((command) => {
|
|
2735
|
+
const fields = [
|
|
2736
|
+
`className: ${JSON.stringify(command.className)}`,
|
|
2737
|
+
`name: ${JSON.stringify(command.name)}`,
|
|
2738
|
+
`permission: ${JSON.stringify(command.permission ?? "")}`,
|
|
2739
|
+
`transaction: ${JSON.stringify(command.transaction)}`,
|
|
2740
|
+
...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
|
|
2741
|
+
`idempotency: ${JSON.stringify(command.idempotency)}`,
|
|
2742
|
+
...command.standalone ? ["standalone: true"] : [],
|
|
2743
|
+
...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
|
|
2744
|
+
];
|
|
2745
|
+
return `{ ${fields.join(", ")} }`;
|
|
2746
|
+
}).join(", ")}]`;
|
|
2747
|
+
}
|
|
2748
|
+
renderJobs() {
|
|
2749
|
+
const jobs = this.module.jobs ?? [];
|
|
2750
|
+
if (jobs.length === 0)
|
|
2751
|
+
return "[]";
|
|
2752
|
+
return `[${jobs.map((job) => `{ className: ${JSON.stringify(job.className)}, name: ${JSON.stringify(job.name)}, serviceKey: ${JSON.stringify(job.serviceKey)}, scope: ${JSON.stringify(job.scope)},${job.aspects && job.aspects.length > 0 ? ` aspects: ${this.renderAspects(job.aspects)},` : ""} }`).join(", ")}]`;
|
|
2753
|
+
}
|
|
2754
|
+
renderAspects(aspects) {
|
|
2755
|
+
return `[${aspects.map((aspect) => this.imports.add(aspect.name, aspect.importPath, aspect.importModule)).join(", ")}]`;
|
|
2756
|
+
}
|
|
1760
2757
|
renderServicesFactory() {
|
|
1761
2758
|
return [
|
|
1762
2759
|
`function create${this.pascal}Services(`,
|
|
@@ -1771,17 +2768,51 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1771
2768
|
renderScopeFactory(kind) {
|
|
1772
2769
|
const suffix = kind === "request" ? "RequestScope" : "JobScope";
|
|
1773
2770
|
return [
|
|
1774
|
-
`function create${this.pascal}${suffix}(`,
|
|
2771
|
+
`async function create${this.pascal}${suffix}(`,
|
|
1775
2772
|
` services: Record<string, unknown>,`,
|
|
1776
2773
|
` ctx: unknown,`,
|
|
1777
2774
|
` imported: Record<string, Record<string, unknown>> = {},`,
|
|
1778
|
-
`): Record<string, unknown
|
|
1779
|
-
|
|
2775
|
+
`): Promise<Record<string, unknown>> {`,
|
|
2776
|
+
` const scope: Record<string, unknown> = {};`,
|
|
2777
|
+
` try {`,
|
|
2778
|
+
indent(this.renderFactoryBody(kind, true), 4),
|
|
2779
|
+
` } catch (error) {`,
|
|
2780
|
+
` try {`,
|
|
2781
|
+
` await destroy${this.pascal}${suffix}(scope);`,
|
|
2782
|
+
` } catch (cleanupError) {`,
|
|
2783
|
+
` console.error("supacloud: ${kind} scope rollback failed for ${this.module.name}", cleanupError);`,
|
|
2784
|
+
` }`,
|
|
2785
|
+
` throw error;`,
|
|
2786
|
+
` }`,
|
|
2787
|
+
`}`
|
|
2788
|
+
].join(`
|
|
2789
|
+
`);
|
|
2790
|
+
}
|
|
2791
|
+
renderScopeDestroyer(kind) {
|
|
2792
|
+
const suffix = kind === "request" ? "RequestScope" : "JobScope";
|
|
2793
|
+
const plan = [];
|
|
2794
|
+
const multiIndices = new Map;
|
|
2795
|
+
for (const provider of orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind))) {
|
|
2796
|
+
const index = provider.multi ? multiIndices.get(provider.token) ?? 0 : undefined;
|
|
2797
|
+
if (index !== undefined)
|
|
2798
|
+
multiIndices.set(provider.token, index + 1);
|
|
2799
|
+
if (provider.kind !== "existing" && provider.hasOnDestroy) {
|
|
2800
|
+
plan.push({ key: camelName(provider.token), index });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
for (const controller of this.module.controllers) {
|
|
2804
|
+
if (factoryOfScope(controller.scope) === kind && controller.hasOnDestroy) {
|
|
2805
|
+
plan.push({ key: camelName(controller.className) });
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
return [
|
|
2809
|
+
`async function destroy${this.pascal}${suffix}(scope: Record<string, unknown>): Promise<void> {`,
|
|
2810
|
+
` await destroyScopeInstances(scope, ${JSON.stringify(plan)});`,
|
|
1780
2811
|
`}`
|
|
1781
2812
|
].join(`
|
|
1782
2813
|
`);
|
|
1783
2814
|
}
|
|
1784
|
-
renderFactoryBody(kind) {
|
|
2815
|
+
renderFactoryBody(kind, scoped = false) {
|
|
1785
2816
|
const providers = orderProviders(this.module.providers.filter((p) => factoryOfScope(p.scope) === kind));
|
|
1786
2817
|
const controllers = this.module.controllers.filter((c) => factoryOfScope(c.scope) === kind);
|
|
1787
2818
|
const lines = [];
|
|
@@ -1792,6 +2823,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1792
2823
|
const emitted = this.emitProvider(provider, kind, true);
|
|
1793
2824
|
if (emitted.constLine)
|
|
1794
2825
|
lines.push(emitted.constLine);
|
|
2826
|
+
if (scoped) {
|
|
2827
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = [...(Array.isArray(scope[${JSON.stringify(emitted.key)}]) ? scope[${JSON.stringify(emitted.key)}] : []), ${emitted.expr}];`);
|
|
2828
|
+
}
|
|
1795
2829
|
const list = multiGroups.get(emitted.key) ?? [];
|
|
1796
2830
|
list.push(emitted.expr);
|
|
1797
2831
|
multiGroups.set(emitted.key, list);
|
|
@@ -1799,6 +2833,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1799
2833
|
const emitted = this.emitProvider(provider, kind, false);
|
|
1800
2834
|
if (emitted.constLine)
|
|
1801
2835
|
lines.push(emitted.constLine);
|
|
2836
|
+
if (scoped) {
|
|
2837
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2838
|
+
}
|
|
1802
2839
|
returns.set(emitted.key, emitted.expr);
|
|
1803
2840
|
}
|
|
1804
2841
|
}
|
|
@@ -1808,8 +2845,16 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1808
2845
|
for (const controller of controllers) {
|
|
1809
2846
|
const emitted = this.emitController(controller, kind);
|
|
1810
2847
|
lines.push(emitted.constLine);
|
|
2848
|
+
if (scoped) {
|
|
2849
|
+
lines.push(`scope[${JSON.stringify(emitted.key)}] = ${emitted.expr};`);
|
|
2850
|
+
}
|
|
1811
2851
|
returns.set(emitted.key, emitted.expr);
|
|
1812
2852
|
}
|
|
2853
|
+
if (scoped) {
|
|
2854
|
+
lines.push(`return scope;`);
|
|
2855
|
+
return lines.join(`
|
|
2856
|
+
`);
|
|
2857
|
+
}
|
|
1813
2858
|
const entries = [...returns.entries()].map(([key, expr]) => key === expr ? key : `${key}: ${expr}`);
|
|
1814
2859
|
lines.push(`return { ${entries.join(", ")} };`);
|
|
1815
2860
|
return lines.join(`
|
|
@@ -1819,39 +2864,76 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1819
2864
|
const key = camelName(provider.token);
|
|
1820
2865
|
switch (provider.kind) {
|
|
1821
2866
|
case "class": {
|
|
1822
|
-
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath);
|
|
1823
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind,
|
|
2867
|
+
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
|
|
2868
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
|
|
1824
2869
|
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
1825
|
-
return {
|
|
2870
|
+
return {
|
|
2871
|
+
constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
|
|
2872
|
+
key,
|
|
2873
|
+
expr: local
|
|
2874
|
+
};
|
|
1826
2875
|
}
|
|
1827
2876
|
case "value": {
|
|
1828
|
-
const expr = provider.importPath ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath) : provider.useValueExpr ?? "undefined";
|
|
2877
|
+
const expr = provider.importPath || provider.importModule ? this.imports.add(provider.useValueExpr ?? "undefined", provider.importPath, provider.importModule) : provider.useValueExpr ?? "undefined";
|
|
1829
2878
|
const local = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
1830
2879
|
return { constLine: `const ${local} = ${expr};`, key, expr: local };
|
|
1831
2880
|
}
|
|
1832
2881
|
case "factory": {
|
|
1833
2882
|
if (provider.tokenKind === "injection-token" && !provider.useFactoryName) {
|
|
1834
|
-
const tokenIdent = this.imports.add(provider.token, provider.importPath);
|
|
2883
|
+
const tokenIdent = this.imports.add(provider.token, provider.importPath, provider.importModule);
|
|
1835
2884
|
const local2 = this.localVar(isMulti ? `${provider.token}Item` : provider.token, kind);
|
|
1836
|
-
const constLine = `const ${local2} =
|
|
2885
|
+
const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
|
|
1837
2886
|
return { constLine, key, expr: local2 };
|
|
1838
2887
|
}
|
|
1839
|
-
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath);
|
|
1840
|
-
const args = provider.deps.map((dep) => this.depExpr(dep, kind,
|
|
2888
|
+
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
2889
|
+
const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
|
|
1841
2890
|
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
1842
2891
|
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
1843
2892
|
}
|
|
1844
2893
|
case "existing": {
|
|
1845
|
-
return {
|
|
2894
|
+
return {
|
|
2895
|
+
key,
|
|
2896
|
+
expr: this.depExpr(provider.useExisting ?? provider.token, kind, this.depOptions(provider, provider.useExisting ?? provider.token))
|
|
2897
|
+
};
|
|
1846
2898
|
}
|
|
1847
2899
|
}
|
|
1848
2900
|
}
|
|
1849
2901
|
emitController(controller, kind) {
|
|
1850
2902
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
1851
|
-
const args = controller.deps.map((dep) => this.depExpr(dep, kind,
|
|
2903
|
+
const args = controller.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
|
|
1852
2904
|
const key = camelName(controller.className);
|
|
1853
2905
|
const local = this.localVar(controller.className, kind);
|
|
1854
|
-
return {
|
|
2906
|
+
return {
|
|
2907
|
+
constLine: `const ${local} = ${this.instantiate(className, args, kind, controller.functionalInjects)};`,
|
|
2908
|
+
key,
|
|
2909
|
+
expr: local
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
instantiate(className, args, kind, functionalInjects) {
|
|
2913
|
+
if (!functionalInjects || functionalInjects.length === 0) {
|
|
2914
|
+
return `new ${className}(${args})`;
|
|
2915
|
+
}
|
|
2916
|
+
const clauses = functionalInjects.map((entry) => {
|
|
2917
|
+
const token = this.imports.add(entry.expression, entry.importPath, entry.importModule);
|
|
2918
|
+
const value = this.depExpr(entry.token, kind, {
|
|
2919
|
+
optional: entry.optional,
|
|
2920
|
+
self: entry.self,
|
|
2921
|
+
skipSelf: entry.skipSelf,
|
|
2922
|
+
host: entry.host
|
|
2923
|
+
});
|
|
2924
|
+
return `if (token === ${token}) return ${value} as T;`;
|
|
2925
|
+
});
|
|
2926
|
+
const missing = `if (options?.optional) return undefined; throw new Error("Static inject token not available: " + String(token));`;
|
|
2927
|
+
const injector = [
|
|
2928
|
+
`{`,
|
|
2929
|
+
`get<T>(token: unknown, options?: { optional?: boolean; self?: boolean; skipSelf?: boolean; host?: boolean }): T | undefined {`,
|
|
2930
|
+
...clauses,
|
|
2931
|
+
missing,
|
|
2932
|
+
`},`,
|
|
2933
|
+
`}`
|
|
2934
|
+
].join(`
|
|
2935
|
+
`);
|
|
2936
|
+
return `runInInjectionContext(${injector}, () => new ${className}(${args}))`;
|
|
1855
2937
|
}
|
|
1856
2938
|
localVar(token, kind) {
|
|
1857
2939
|
const locals = this.locals[kind];
|
|
@@ -1868,23 +2950,34 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1868
2950
|
locals.set(token, local);
|
|
1869
2951
|
return local;
|
|
1870
2952
|
}
|
|
1871
|
-
|
|
2953
|
+
depOptions(node, token) {
|
|
2954
|
+
return {
|
|
2955
|
+
optional: node.optionalDeps?.includes(token) ?? false,
|
|
2956
|
+
self: node.selfDeps?.includes(token) ?? false,
|
|
2957
|
+
skipSelf: node.skipSelfDeps?.includes(token) ?? false,
|
|
2958
|
+
host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
depExpr(token, kind, options = {}) {
|
|
2962
|
+
const isOptional = options.optional ?? false;
|
|
2963
|
+
const isSelf = options.self ?? false;
|
|
2964
|
+
const isSkipSelf = options.skipSelf ?? false;
|
|
1872
2965
|
if (kind === "request" && isRequestContextToken(token, this.graph.tokenNames))
|
|
1873
2966
|
return "ctx";
|
|
1874
2967
|
if (kind === "job" && isJobContextToken(token, this.graph.tokenNames))
|
|
1875
2968
|
return "ctx";
|
|
1876
2969
|
const own = this.module.providers.find((p) => p.token === token);
|
|
1877
|
-
|
|
2970
|
+
const ownIsLocal = own && factoryOfScope(own.scope) === kind;
|
|
2971
|
+
if (own && ownIsLocal && !isSkipSelf) {
|
|
1878
2972
|
if (factoryOfScope(own.scope) === kind && own.kind !== "existing") {
|
|
1879
2973
|
return this.locals[kind].get(token) ?? camelName(token);
|
|
1880
2974
|
}
|
|
1881
|
-
if (own.kind === "existing"
|
|
1882
|
-
return this.depExpr(own.useExisting ?? token, kind,
|
|
2975
|
+
if (own.kind === "existing") {
|
|
2976
|
+
return this.depExpr(own.useExisting ?? token, kind, options);
|
|
1883
2977
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
1887
|
-
return `services.${camelName(token)}`;
|
|
2978
|
+
}
|
|
2979
|
+
if (isSelf) {
|
|
2980
|
+
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
1888
2981
|
}
|
|
1889
2982
|
for (const importName of this.module.imports) {
|
|
1890
2983
|
const imported = this.graph.modules.find((m) => m.name === importName);
|
|
@@ -1903,6 +2996,8 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1903
2996
|
if (isOptional && !this.graph.externalTokens.includes(token)) {
|
|
1904
2997
|
return "undefined";
|
|
1905
2998
|
}
|
|
2999
|
+
if (isSelf)
|
|
3000
|
+
return isOptional ? "undefined" : `services.${camelName(token)}`;
|
|
1906
3001
|
if (kind === "services")
|
|
1907
3002
|
return isOptional ? `(deps.${camelName(token)} ?? undefined)` : `deps.${camelName(token)}`;
|
|
1908
3003
|
return isOptional ? `(services.${camelName(token)} ?? undefined)` : `services.${camelName(token)}`;
|
|
@@ -2360,12 +3455,17 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
2360
3455
|
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
2361
3456
|
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
2362
3457
|
"missing-token-factory": { code: "SC2009", docsUrl: "https://supacloud.dev/errors/SC2009" },
|
|
3458
|
+
"provider-type-mismatch": { code: "SC2010", docsUrl: "https://supacloud.dev/errors/SC2010" },
|
|
3459
|
+
"unsupported-provider-helper": { code: "SC2011", docsUrl: "https://supacloud.dev/errors/SC2011" },
|
|
2363
3460
|
"command-missing-permission": { code: "SC4001", docsUrl: "https://supacloud.dev/errors/SC4001" },
|
|
2364
3461
|
"duplicate-command": { code: "SC4002", docsUrl: "https://supacloud.dev/errors/SC4002" },
|
|
2365
3462
|
"route-command-unresolved": { code: "SC4003", docsUrl: "https://supacloud.dev/errors/SC4003" },
|
|
2366
3463
|
"command-governance-unsupported": { code: "SC4004", docsUrl: "https://supacloud.dev/errors/SC4004" },
|
|
2367
3464
|
"route-command-binding-disabled": { code: "SC4005", docsUrl: "https://supacloud.dev/errors/SC4005" },
|
|
2368
3465
|
"command-transaction-readonly": { code: "SC4006", docsUrl: "https://supacloud.dev/errors/SC4006" },
|
|
3466
|
+
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3467
|
+
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3468
|
+
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
2369
3469
|
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
2370
3470
|
};
|
|
2371
3471
|
function validateGraph(graph, options = false) {
|
|
@@ -2399,10 +3499,14 @@ function validateGraph(graph, options = false) {
|
|
|
2399
3499
|
}
|
|
2400
3500
|
}
|
|
2401
3501
|
}
|
|
2402
|
-
function resolveDep(module, token) {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
3502
|
+
function resolveDep(module, token, flags = {}) {
|
|
3503
|
+
if (!flags.skipSelf) {
|
|
3504
|
+
const own = module.providers.find((p) => p.token === token);
|
|
3505
|
+
if (own)
|
|
3506
|
+
return { module, provider: own };
|
|
3507
|
+
}
|
|
3508
|
+
if (flags.self)
|
|
3509
|
+
return;
|
|
2406
3510
|
for (const importName of module.imports) {
|
|
2407
3511
|
const imported = graph.modules.find((m) => m.name === importName);
|
|
2408
3512
|
if (!imported || !imported.exports.includes(token))
|
|
@@ -2600,7 +3704,7 @@ function validateGraph(graph, options = false) {
|
|
|
2600
3704
|
}
|
|
2601
3705
|
if (controller.selfDeps && controller.selfDeps.length > 0) {
|
|
2602
3706
|
for (const dep of controller.selfDeps) {
|
|
2603
|
-
const own = module.providers.find((p) => p.token === dep);
|
|
3707
|
+
const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
|
|
2604
3708
|
if (!own) {
|
|
2605
3709
|
error("self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, controller.file, undefined, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2606
3710
|
}
|
|
@@ -2608,7 +3712,7 @@ function validateGraph(graph, options = false) {
|
|
|
2608
3712
|
}
|
|
2609
3713
|
if (controller.skipSelfDeps && controller.skipSelfDeps.length > 0) {
|
|
2610
3714
|
for (const dep of controller.skipSelfDeps) {
|
|
2611
|
-
const own = module.providers.find((p) => p.token === dep);
|
|
3715
|
+
const own = module.providers.find((p) => p.token === dep && p.scope === controller.scope);
|
|
2612
3716
|
if (own) {
|
|
2613
3717
|
error("skip-self-resolution-failed", `模块 ${module.name} 的 controller ${controller.className} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, controller.file, undefined, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
|
|
2614
3718
|
}
|
|
@@ -2683,7 +3787,7 @@ function validateGraph(graph, options = false) {
|
|
|
2683
3787
|
for (const provider of module.providers) {
|
|
2684
3788
|
if (provider.selfDeps && provider.selfDeps.length > 0) {
|
|
2685
3789
|
for (const dep of provider.selfDeps) {
|
|
2686
|
-
const own = module.providers.find((p) => p.token === dep);
|
|
3790
|
+
const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
|
|
2687
3791
|
if (!own) {
|
|
2688
3792
|
error("self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @Self(),但 ${dep} 未在当前模块内部提供`, provider.file, provider.line, `Provide '${dep}' in module '${module.name}' or remove @Self().`);
|
|
2689
3793
|
}
|
|
@@ -2691,7 +3795,7 @@ function validateGraph(graph, options = false) {
|
|
|
2691
3795
|
}
|
|
2692
3796
|
if (provider.skipSelfDeps && provider.skipSelfDeps.length > 0) {
|
|
2693
3797
|
for (const dep of provider.skipSelfDeps) {
|
|
2694
|
-
const own = module.providers.find((p) => p.token === dep);
|
|
3798
|
+
const own = module.providers.find((p) => p.token === dep && p.scope === provider.scope);
|
|
2695
3799
|
if (own) {
|
|
2696
3800
|
error("skip-self-resolution-failed", `模块 ${module.name} 的 provider ${provider.token} 参数标记了 @SkipSelf(),但 ${dep} 在当前模块内部声明了 provider`, provider.file, provider.line, `Remove '${dep}' from module '${module.name}' providers or remove @SkipSelf().`);
|
|
2697
3801
|
}
|
|
@@ -2699,7 +3803,10 @@ function validateGraph(graph, options = false) {
|
|
|
2699
3803
|
}
|
|
2700
3804
|
for (const dep of provider.deps) {
|
|
2701
3805
|
const isOptional = provider.optionalDeps?.includes(dep);
|
|
2702
|
-
const resolved = resolveDep(module, dep
|
|
3806
|
+
const resolved = resolveDep(module, dep, {
|
|
3807
|
+
self: provider.selfDeps?.includes(dep),
|
|
3808
|
+
skipSelf: provider.skipSelfDeps?.includes(dep)
|
|
3809
|
+
});
|
|
2703
3810
|
if (!resolved) {
|
|
2704
3811
|
if (isOptional) {
|
|
2705
3812
|
continue;
|
|
@@ -2707,6 +3814,8 @@ function validateGraph(graph, options = false) {
|
|
|
2707
3814
|
if (!graph.externalTokens.includes(dep)) {
|
|
2708
3815
|
if (globalProviders.has(dep)) {
|
|
2709
3816
|
const owner = globalProviders.get(dep);
|
|
3817
|
+
if (!owner)
|
|
3818
|
+
continue;
|
|
2710
3819
|
error("module-boundary", `模块 ${module.name} 的 provider ${provider.token} 依赖 ${dep},该 token 由模块 ${owner.module.name} 提供但未被 import`, provider.file, provider.line, `Import module '${owner.module.name}' in '${module.name}', add '${dep}' to '${owner.module.name}' exports, or mark @Injectable({ providedIn: 'root' }).`);
|
|
2711
3820
|
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
2712
3821
|
error("missing-token-factory", `InjectionToken '${dep}' referenced by provider '${provider.token}' has no provider in module '${module.name}' and no default factory function.`, provider.file, provider.line, `Provide '${dep}' in @Module({ providers: [...] }) or declare it with new InjectionToken('${dep}', { factory: () => ... }).`);
|
|
@@ -2767,7 +3876,8 @@ function validateGraph(graph, options = false) {
|
|
|
2767
3876
|
}
|
|
2768
3877
|
}
|
|
2769
3878
|
if (rule.onlyDependOnLibsWithTags && rule.onlyDependOnLibsWithTags.length > 0) {
|
|
2770
|
-
const
|
|
3879
|
+
const allowedTags = rule.onlyDependOnLibsWithTags;
|
|
3880
|
+
const hasAllowed = targetTags.some((t) => allowedTags.includes(t));
|
|
2771
3881
|
if (!hasAllowed && targetTags.length > 0) {
|
|
2772
3882
|
error("module-boundary-violation", `模块 ${module.name} (tags: [${sourceTags.join(", ")}]) 仅允许依赖带有 [${rule.onlyDependOnLibsWithTags.join(", ")}] 标签的模块,但模块 ${targetModule.name} 的标签为 [${targetTags.join(", ")}]`, module.file, module.line);
|
|
2773
3883
|
}
|
|
@@ -2789,6 +3899,8 @@ function validateGraph(graph, options = false) {
|
|
|
2789
3899
|
referencedTokens.add(d);
|
|
2790
3900
|
for (const d of ctrl.skipSelfDeps ?? [])
|
|
2791
3901
|
referencedTokens.add(d);
|
|
3902
|
+
for (const d of ctrl.hostDeps ?? [])
|
|
3903
|
+
referencedTokens.add(d);
|
|
2792
3904
|
}
|
|
2793
3905
|
for (const p of mod.providers) {
|
|
2794
3906
|
for (const d of p.deps ?? [])
|
|
@@ -2799,6 +3911,8 @@ function validateGraph(graph, options = false) {
|
|
|
2799
3911
|
referencedTokens.add(d);
|
|
2800
3912
|
for (const d of p.skipSelfDeps ?? [])
|
|
2801
3913
|
referencedTokens.add(d);
|
|
3914
|
+
for (const d of p.hostDeps ?? [])
|
|
3915
|
+
referencedTokens.add(d);
|
|
2802
3916
|
if (p.useExisting)
|
|
2803
3917
|
referencedTokens.add(p.useExisting);
|
|
2804
3918
|
}
|
|
@@ -2921,7 +4035,10 @@ function detectCycles(graph, resolveDep) {
|
|
|
2921
4035
|
state.set(id, "visiting");
|
|
2922
4036
|
stack.push(ref);
|
|
2923
4037
|
for (const dep of ref.provider.deps) {
|
|
2924
|
-
const resolved = resolveDep(ref.module, dep
|
|
4038
|
+
const resolved = resolveDep(ref.module, dep, {
|
|
4039
|
+
self: ref.provider.selfDeps?.includes(dep),
|
|
4040
|
+
skipSelf: ref.provider.skipSelfDeps?.includes(dep)
|
|
4041
|
+
});
|
|
2925
4042
|
if (resolved)
|
|
2926
4043
|
visit(resolved);
|
|
2927
4044
|
}
|
|
@@ -3034,6 +4151,8 @@ function detectOrphanModules(graph) {
|
|
|
3034
4151
|
}
|
|
3035
4152
|
while (queue.length > 0) {
|
|
3036
4153
|
const current = queue.shift();
|
|
4154
|
+
if (!current)
|
|
4155
|
+
continue;
|
|
3037
4156
|
const mod = moduleMap.get(current);
|
|
3038
4157
|
if (!mod)
|
|
3039
4158
|
continue;
|
|
@@ -3062,10 +4181,225 @@ function detectOrphanModules(graph) {
|
|
|
3062
4181
|
}
|
|
3063
4182
|
|
|
3064
4183
|
// src/compile.ts
|
|
3065
|
-
import { existsSync as
|
|
3066
|
-
import { join as
|
|
4184
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
4185
|
+
import { join as join4 } from "node:path";
|
|
4186
|
+
|
|
4187
|
+
// src/type-safety.ts
|
|
4188
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4189
|
+
import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
4190
|
+
import * as ts4 from "@typescript/typescript6";
|
|
4191
|
+
var DEFAULT_EXCLUDES = [
|
|
4192
|
+
"**/*.test.ts",
|
|
4193
|
+
"**/*.spec.ts",
|
|
4194
|
+
"**/test/**",
|
|
4195
|
+
"**/tests/**",
|
|
4196
|
+
"**/__tests__/**",
|
|
4197
|
+
"**/fixtures/**",
|
|
4198
|
+
"**/generated/**",
|
|
4199
|
+
"**/dist/**",
|
|
4200
|
+
"**/*.d.ts"
|
|
4201
|
+
];
|
|
4202
|
+
var DIAGNOSTIC_META = {
|
|
4203
|
+
"generated-any": { errorCode: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
|
|
4204
|
+
"source-any": { errorCode: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
|
|
4205
|
+
"source-type-assertion": { errorCode: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
|
|
4206
|
+
"source-non-null-assertion": { errorCode: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
|
|
4207
|
+
"source-implicit-widening": { errorCode: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" }
|
|
4208
|
+
};
|
|
4209
|
+
function scanGeneratedArtifacts(artifacts, strict = true) {
|
|
4210
|
+
const diagnostics = [];
|
|
4211
|
+
for (const [file, content] of Object.entries(artifacts)) {
|
|
4212
|
+
if (content === undefined)
|
|
4213
|
+
continue;
|
|
4214
|
+
const sourceFile = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
4215
|
+
for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
|
|
4216
|
+
diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
return diagnostics;
|
|
4220
|
+
}
|
|
4221
|
+
function scanProductionSource(options) {
|
|
4222
|
+
const rootDir = resolve2(options.rootDir);
|
|
4223
|
+
const configPath = join3(rootDir, "tsconfig.json");
|
|
4224
|
+
const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
|
|
4225
|
+
options: {
|
|
4226
|
+
strict: true,
|
|
4227
|
+
skipLibCheck: true,
|
|
4228
|
+
target: ts4.ScriptTarget.ES2022,
|
|
4229
|
+
module: ts4.ModuleKind.ESNext
|
|
4230
|
+
},
|
|
4231
|
+
errors: []
|
|
4232
|
+
};
|
|
4233
|
+
const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
|
|
4234
|
+
const rootNames = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
|
|
4235
|
+
const compilerOptions = { ...projectConfig.options, noEmit: true };
|
|
4236
|
+
const host = ts4.createCompilerHost(compilerOptions);
|
|
4237
|
+
host.getCurrentDirectory = () => rootDir;
|
|
4238
|
+
const program = ts4.createProgram(rootNames, compilerOptions, host);
|
|
4239
|
+
const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
|
|
4240
|
+
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
4241
|
+
const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
|
|
4242
|
+
const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
|
|
4243
|
+
severity: "error",
|
|
4244
|
+
code: "source-config",
|
|
4245
|
+
message: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
|
|
4246
|
+
`),
|
|
4247
|
+
file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
|
|
4248
|
+
line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
|
|
4249
|
+
errorCode: `TS${diagnostic.code}`
|
|
4250
|
+
}));
|
|
4251
|
+
const checker = program.getTypeChecker();
|
|
4252
|
+
for (const sourceFile of sourceFiles) {
|
|
4253
|
+
scanSourceFile(sourceFile, checker, rootDir, diagnostics, options.strict ?? false);
|
|
4254
|
+
}
|
|
4255
|
+
return diagnostics;
|
|
4256
|
+
}
|
|
4257
|
+
function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
4258
|
+
for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
|
|
4259
|
+
diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
|
|
4260
|
+
}
|
|
4261
|
+
for (const node of descendants(sourceFile)) {
|
|
4262
|
+
if (ts4.isAsExpression(node)) {
|
|
4263
|
+
if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
|
|
4264
|
+
continue;
|
|
4265
|
+
const assertedType = node.type.getText(sourceFile);
|
|
4266
|
+
if (assertedType === "const")
|
|
4267
|
+
continue;
|
|
4268
|
+
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4269
|
+
} else if (ts4.isTypeAssertionExpression(node)) {
|
|
4270
|
+
if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
|
|
4271
|
+
continue;
|
|
4272
|
+
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4273
|
+
} else if (ts4.isNonNullExpression(node)) {
|
|
4274
|
+
diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
for (const declaration of descendantsOfKind2(sourceFile, ts4.isVariableDeclaration)) {
|
|
4278
|
+
const initializer = declaration.initializer;
|
|
4279
|
+
if (!initializer || declaration.type)
|
|
4280
|
+
continue;
|
|
4281
|
+
const declarationType = checker.getTypeAtLocation(declaration.name);
|
|
4282
|
+
const initializerType = checker.getTypeAtLocation(initializer);
|
|
4283
|
+
for (const name of bindingNames(declaration.name)) {
|
|
4284
|
+
if (isAnyType(checker.getTypeAtLocation(name))) {
|
|
4285
|
+
diagnostics.push(makeDiagnostic("source-any", "生产源码中的变量被推断为 any;请为边界数据提供解析类型或显式 unknown。", sourceFile, name, strict, rootDir));
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
if (isAnyType(declarationType))
|
|
4289
|
+
continue;
|
|
4290
|
+
if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
|
|
4291
|
+
diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
|
|
4292
|
+
}
|
|
4293
|
+
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))) {
|
|
4294
|
+
diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
for (const parameter of descendantsOfKind2(sourceFile, ts4.isParameter)) {
|
|
4298
|
+
if (parameter.type)
|
|
4299
|
+
continue;
|
|
4300
|
+
for (const name of bindingNames(parameter.name)) {
|
|
4301
|
+
if (isAnyType(checker.getTypeAtLocation(name))) {
|
|
4302
|
+
diagnostics.push(makeDiagnostic("source-any", "生产源码中的参数被推断为 any;请补充参数类型。", sourceFile, name, strict, rootDir));
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
function readProjectConfig2(configPath) {
|
|
4308
|
+
const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
|
|
4309
|
+
if (config.error)
|
|
4310
|
+
return { options: {}, errors: [config.error] };
|
|
4311
|
+
const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
|
|
4312
|
+
return { options: parsed.options, errors: parsed.errors };
|
|
4313
|
+
}
|
|
4314
|
+
function isProductionSource(rootDir, sourceFile, excludes, outDir) {
|
|
4315
|
+
const relativePath = normalizeRelative(rootDir, sourceFile.fileName);
|
|
4316
|
+
if (sourceFile.isDeclarationFile || relativePath.startsWith("../") || relativePath.includes("node_modules/"))
|
|
4317
|
+
return false;
|
|
4318
|
+
if (outDir && (relativePath === outDir || relativePath.startsWith(`${outDir}/`)))
|
|
4319
|
+
return false;
|
|
4320
|
+
return !excludes.some((pattern) => globMatches(relativePath, pattern));
|
|
4321
|
+
}
|
|
4322
|
+
function isProductionSourcePath(rootDir, filePath, excludes) {
|
|
4323
|
+
const relativePath = normalizeRelative(rootDir, filePath);
|
|
4324
|
+
return !relativePath.startsWith("../") && !relativePath.includes("node_modules/") && !excludes.some((pattern) => globMatches(relativePath, pattern));
|
|
4325
|
+
}
|
|
4326
|
+
function globMatches(value, pattern) {
|
|
4327
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "§/").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/§\//g, "(?:.*/)?").replace(/§§/g, ".*");
|
|
4328
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
4329
|
+
}
|
|
4330
|
+
function bindingNames(name) {
|
|
4331
|
+
if (ts4.isIdentifier(name))
|
|
4332
|
+
return [name];
|
|
4333
|
+
return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
|
|
4334
|
+
}
|
|
4335
|
+
function isLiteralExpression(node) {
|
|
4336
|
+
if (!node)
|
|
4337
|
+
return false;
|
|
4338
|
+
return [
|
|
4339
|
+
ts4.SyntaxKind.StringLiteral,
|
|
4340
|
+
ts4.SyntaxKind.NumericLiteral,
|
|
4341
|
+
ts4.SyntaxKind.TrueKeyword,
|
|
4342
|
+
ts4.SyntaxKind.FalseKeyword
|
|
4343
|
+
].includes(node.kind);
|
|
4344
|
+
}
|
|
4345
|
+
function isLiteralSyntax(node) {
|
|
4346
|
+
return ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
|
|
4347
|
+
}
|
|
4348
|
+
function isLiteralType(type) {
|
|
4349
|
+
return (type.flags & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
|
|
4350
|
+
}
|
|
4351
|
+
function isAnyType(type) {
|
|
4352
|
+
return (type.flags & ts4.TypeFlags.Any) !== 0;
|
|
4353
|
+
}
|
|
4354
|
+
function isLetDeclaration(declaration) {
|
|
4355
|
+
return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
|
|
4356
|
+
}
|
|
4357
|
+
function isConstDeclaration(declaration) {
|
|
4358
|
+
return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
|
|
4359
|
+
}
|
|
4360
|
+
function descendants(root) {
|
|
4361
|
+
const result = [];
|
|
4362
|
+
const visit = (node) => {
|
|
4363
|
+
result.push(node);
|
|
4364
|
+
ts4.forEachChild(node, visit);
|
|
4365
|
+
};
|
|
4366
|
+
ts4.forEachChild(root, visit);
|
|
4367
|
+
return result;
|
|
4368
|
+
}
|
|
4369
|
+
function descendantsOfKind2(root, predicate) {
|
|
4370
|
+
const result = [];
|
|
4371
|
+
const visit = (node) => {
|
|
4372
|
+
if (predicate(node))
|
|
4373
|
+
result.push(node);
|
|
4374
|
+
ts4.forEachChild(node, visit);
|
|
4375
|
+
};
|
|
4376
|
+
ts4.forEachChild(root, visit);
|
|
4377
|
+
return result;
|
|
4378
|
+
}
|
|
4379
|
+
function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
|
|
4380
|
+
const sourceFile = typeof fileOrSourceFile === "string" ? undefined : fileOrSourceFile;
|
|
4381
|
+
const file = typeof fileOrSourceFile === "string" ? fileOrSourceFile : rootDir ? normalizeRelative(rootDir, fileOrSourceFile.fileName) : fileOrSourceFile.fileName;
|
|
4382
|
+
const meta = DIAGNOSTIC_META[code];
|
|
4383
|
+
return {
|
|
4384
|
+
severity: strict ? "error" : "warn",
|
|
4385
|
+
code,
|
|
4386
|
+
message,
|
|
4387
|
+
file,
|
|
4388
|
+
line: sourceFile ? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 : undefined,
|
|
4389
|
+
errorCode: meta.errorCode,
|
|
4390
|
+
docsUrl: meta.docsUrl
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
function normalizeRelative(rootDir, filePath) {
|
|
4394
|
+
return relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
|
|
4395
|
+
}
|
|
4396
|
+
function isAnyKeyword(node) {
|
|
4397
|
+
return node.kind === ts4.SyntaxKind.AnyKeyword;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
// src/compile.ts
|
|
3067
4401
|
async function compileProject(options) {
|
|
3068
|
-
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
4402
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
3069
4403
|
const diagnostics = [
|
|
3070
4404
|
...graph.diagnostics ?? [],
|
|
3071
4405
|
...validateGraph(graph, {
|
|
@@ -3084,14 +4418,40 @@ async function compileProject(options) {
|
|
|
3084
4418
|
diagnostic.severity = "error";
|
|
3085
4419
|
}
|
|
3086
4420
|
}
|
|
3087
|
-
const
|
|
3088
|
-
const
|
|
4421
|
+
const typeSafety = resolveTypeSafety(options);
|
|
4422
|
+
const rendered = renderApplication(graph, {
|
|
3089
4423
|
rootDir: options.rootDir,
|
|
3090
4424
|
outDir: options.outDir,
|
|
3091
4425
|
generateClient: options.generateClient,
|
|
3092
4426
|
generatePermissions: options.generatePermissions,
|
|
3093
4427
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
3094
|
-
})
|
|
4428
|
+
});
|
|
4429
|
+
if (typeSafety.scanProductionSource) {
|
|
4430
|
+
diagnostics.push(...scanProductionSource({
|
|
4431
|
+
rootDir: options.rootDir,
|
|
4432
|
+
include: options.include,
|
|
4433
|
+
outDir: options.outDir,
|
|
4434
|
+
strict: options.strict,
|
|
4435
|
+
...typeSafety
|
|
4436
|
+
}));
|
|
4437
|
+
}
|
|
4438
|
+
if (typeSafety.noAnyInGenerated) {
|
|
4439
|
+
diagnostics.push(...scanGeneratedArtifacts({
|
|
4440
|
+
"application.ts": rendered.applicationCode,
|
|
4441
|
+
"client.ts": rendered.clientCode,
|
|
4442
|
+
"permissions.ts": rendered.permissionsCode
|
|
4443
|
+
}, options.strict ?? false));
|
|
4444
|
+
}
|
|
4445
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
4446
|
+
const generatedOptions = {
|
|
4447
|
+
rootDir: options.rootDir,
|
|
4448
|
+
outDir: options.outDir,
|
|
4449
|
+
generateClient: options.generateClient,
|
|
4450
|
+
generatePermissions: options.generatePermissions,
|
|
4451
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
4452
|
+
artifactHashes: options.cache?.generatedHashes
|
|
4453
|
+
};
|
|
4454
|
+
const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
|
|
3095
4455
|
const stats = graph.cacheStats ? {
|
|
3096
4456
|
cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
|
|
3097
4457
|
changedFiles: [],
|
|
@@ -3102,7 +4462,7 @@ async function compileProject(options) {
|
|
|
3102
4462
|
return { diagnostics, graph, written, stats };
|
|
3103
4463
|
}
|
|
3104
4464
|
async function checkProject(options) {
|
|
3105
|
-
const graph = await analyzeProject(options.rootDir, options.include, options.cache);
|
|
4465
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
3106
4466
|
const diagnostics = [
|
|
3107
4467
|
...graph.diagnostics ?? [],
|
|
3108
4468
|
...validateGraph(graph, {
|
|
@@ -3121,6 +4481,7 @@ async function checkProject(options) {
|
|
|
3121
4481
|
diagnostic.severity = "error";
|
|
3122
4482
|
}
|
|
3123
4483
|
}
|
|
4484
|
+
const typeSafety = resolveTypeSafety(options);
|
|
3124
4485
|
const rendered = renderApplication(graph, {
|
|
3125
4486
|
rootDir: options.rootDir,
|
|
3126
4487
|
outDir: options.outDir,
|
|
@@ -3128,6 +4489,22 @@ async function checkProject(options) {
|
|
|
3128
4489
|
generatePermissions: options.generatePermissions,
|
|
3129
4490
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
3130
4491
|
});
|
|
4492
|
+
if (typeSafety.scanProductionSource) {
|
|
4493
|
+
diagnostics.push(...scanProductionSource({
|
|
4494
|
+
rootDir: options.rootDir,
|
|
4495
|
+
include: options.include,
|
|
4496
|
+
outDir: options.outDir,
|
|
4497
|
+
strict: options.strict,
|
|
4498
|
+
...typeSafety
|
|
4499
|
+
}));
|
|
4500
|
+
}
|
|
4501
|
+
if (typeSafety.noAnyInGenerated) {
|
|
4502
|
+
diagnostics.push(...scanGeneratedArtifacts({
|
|
4503
|
+
"application.ts": rendered.applicationCode,
|
|
4504
|
+
"client.ts": rendered.clientCode,
|
|
4505
|
+
"permissions.ts": rendered.permissionsCode
|
|
4506
|
+
}, options.strict ?? false));
|
|
4507
|
+
}
|
|
3131
4508
|
const expectedFiles = {
|
|
3132
4509
|
"application.ts": rendered.applicationCode,
|
|
3133
4510
|
"app.manifest.json": rendered.manifestJson
|
|
@@ -3140,12 +4517,12 @@ async function checkProject(options) {
|
|
|
3140
4517
|
}
|
|
3141
4518
|
const mismatches = [];
|
|
3142
4519
|
for (const [filename, expectedContent] of Object.entries(expectedFiles)) {
|
|
3143
|
-
const diskPath =
|
|
3144
|
-
if (!
|
|
4520
|
+
const diskPath = join4(options.outDir, filename);
|
|
4521
|
+
if (!existsSync3(diskPath)) {
|
|
3145
4522
|
mismatches.push(`${filename}: generated artifact is missing from disk`);
|
|
3146
4523
|
continue;
|
|
3147
4524
|
}
|
|
3148
|
-
const diskContent =
|
|
4525
|
+
const diskContent = readFileSync3(diskPath, "utf8");
|
|
3149
4526
|
if (diskContent !== expectedContent) {
|
|
3150
4527
|
mismatches.push(`${filename}: disk artifact differs from current compiler output`);
|
|
3151
4528
|
}
|
|
@@ -3157,29 +4534,40 @@ async function checkProject(options) {
|
|
|
3157
4534
|
graph
|
|
3158
4535
|
};
|
|
3159
4536
|
}
|
|
4537
|
+
function resolveTypeSafety(options) {
|
|
4538
|
+
return {
|
|
4539
|
+
noAnyInGenerated: options.typeSafety?.noAnyInGenerated ?? options.strict ?? false,
|
|
4540
|
+
scanProductionSource: options.typeSafety?.scanProductionSource ?? options.strict ?? false,
|
|
4541
|
+
exclude: options.typeSafety?.exclude
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
3160
4544
|
// src/watch.ts
|
|
3161
4545
|
import { watch } from "node:fs";
|
|
3162
|
-
import { relative as
|
|
4546
|
+
import { relative as relative4, resolve as resolve4 } from "node:path";
|
|
3163
4547
|
|
|
3164
4548
|
// src/incremental.ts
|
|
3165
|
-
import { createHash as
|
|
3166
|
-
import { access, readdir, readFile } from "node:fs/promises";
|
|
3167
|
-
import { relative as
|
|
4549
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
4550
|
+
import { access as access2, readdir, readFile } from "node:fs/promises";
|
|
4551
|
+
import { isAbsolute, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
|
|
3168
4552
|
function createDependencyGraphCache() {
|
|
3169
4553
|
return {
|
|
3170
4554
|
modules: new Map,
|
|
3171
|
-
fileHashes: new Map
|
|
4555
|
+
fileHashes: new Map,
|
|
4556
|
+
generatedHashes: new Map
|
|
3172
4557
|
};
|
|
3173
4558
|
}
|
|
3174
4559
|
function createIncrementalCompiler() {
|
|
3175
4560
|
let previousSnapshot;
|
|
3176
4561
|
let previousResult;
|
|
4562
|
+
let previousCache;
|
|
3177
4563
|
const cache = createDependencyGraphCache();
|
|
3178
4564
|
return {
|
|
3179
4565
|
async compile(options, changedPaths) {
|
|
3180
|
-
const
|
|
4566
|
+
const optionsKey = optionsKeyOf(options);
|
|
4567
|
+
const snapshot = changedPaths && previousSnapshot && previousSnapshot.optionsKey === optionsKey ? await updateSnapshot(previousSnapshot, options, changedPaths) : await createSnapshot(options);
|
|
3181
4568
|
const changedFiles = changedPaths && previousSnapshot ? diffFiles(previousSnapshot.files, snapshot.files) : diffFiles(previousSnapshot?.files, snapshot.files);
|
|
3182
|
-
const
|
|
4569
|
+
const activeCache = options.cache ?? cache;
|
|
4570
|
+
const cacheHit = Boolean(previousSnapshot && previousSnapshot.optionsKey === snapshot.optionsKey && previousCache === activeCache && changedFiles.length === 0);
|
|
3183
4571
|
if (cacheHit && previousResult) {
|
|
3184
4572
|
return {
|
|
3185
4573
|
...previousResult,
|
|
@@ -3192,22 +4580,14 @@ function createIncrementalCompiler() {
|
|
|
3192
4580
|
}
|
|
3193
4581
|
};
|
|
3194
4582
|
}
|
|
3195
|
-
if (previousResult && previousSnapshot && changedFiles.length > 0 && !await requiresGraphRebuild(options.rootDir, changedFiles)) {
|
|
3196
|
-
const stats2 = {
|
|
3197
|
-
cacheHit: true,
|
|
3198
|
-
changedFiles,
|
|
3199
|
-
affectedModules: findAffectedModules(previousResult.graph.modules, previousResult.graph.modules, changedFiles),
|
|
3200
|
-
reusedModules: previousResult.graph.modules.map((m) => m.name),
|
|
3201
|
-
reanalyzedModules: []
|
|
3202
|
-
};
|
|
3203
|
-
previousSnapshot = snapshot;
|
|
3204
|
-
return { ...previousResult, written: [], stats: stats2 };
|
|
3205
|
-
}
|
|
3206
|
-
const activeCache = options.cache ?? cache;
|
|
3207
4583
|
if (!activeCache.dependencyGraph && previousResult) {
|
|
3208
4584
|
activeCache.dependencyGraph = new ModuleDependencyGraph(previousResult.graph.modules);
|
|
3209
4585
|
}
|
|
3210
|
-
const result = await compileProject({
|
|
4586
|
+
const result = await compileProject({
|
|
4587
|
+
...options,
|
|
4588
|
+
cache: activeCache,
|
|
4589
|
+
changedPaths: changedFiles
|
|
4590
|
+
});
|
|
3211
4591
|
const affectedModules = previousResult ? findAffectedModules(previousResult.graph.modules, result.graph.modules, changedFiles) : result.graph.modules.map((module) => module.name);
|
|
3212
4592
|
const reusedModules = result.graph.cacheStats?.reusedModules ?? [];
|
|
3213
4593
|
const reanalyzedModules = result.graph.cacheStats?.reanalyzedModules ?? affectedModules;
|
|
@@ -3223,14 +4603,18 @@ function createIncrementalCompiler() {
|
|
|
3223
4603
|
};
|
|
3224
4604
|
previousSnapshot = snapshot;
|
|
3225
4605
|
previousResult = result;
|
|
4606
|
+
previousCache = activeCache;
|
|
3226
4607
|
return { ...result, stats };
|
|
3227
4608
|
},
|
|
3228
4609
|
reset() {
|
|
3229
4610
|
previousSnapshot = undefined;
|
|
3230
4611
|
previousResult = undefined;
|
|
4612
|
+
previousCache = undefined;
|
|
3231
4613
|
cache.modules.clear();
|
|
3232
4614
|
cache.fileHashes.clear();
|
|
4615
|
+
cache.generatedHashes?.clear();
|
|
3233
4616
|
cache.dependencyGraph = undefined;
|
|
4617
|
+
cache.programSession?.reset();
|
|
3234
4618
|
},
|
|
3235
4619
|
getCache() {
|
|
3236
4620
|
return cache;
|
|
@@ -3238,18 +4622,21 @@ function createIncrementalCompiler() {
|
|
|
3238
4622
|
};
|
|
3239
4623
|
}
|
|
3240
4624
|
async function updateSnapshot(previous, options, changedPaths) {
|
|
3241
|
-
const rootDir =
|
|
3242
|
-
const outDir =
|
|
4625
|
+
const rootDir = resolve3(options.rootDir);
|
|
4626
|
+
const outDir = resolve3(options.outDir);
|
|
3243
4627
|
const files = { ...previous.files };
|
|
3244
4628
|
for (const changedPath of changedPaths) {
|
|
3245
|
-
const absolutePath =
|
|
4629
|
+
const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
|
|
4630
|
+
const relativeChangedPath = relative3(rootDir, absolutePath);
|
|
4631
|
+
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
|
|
4632
|
+
continue;
|
|
3246
4633
|
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
3247
4634
|
continue;
|
|
3248
|
-
const relativePath =
|
|
4635
|
+
const relativePath = relative3(rootDir, absolutePath).split(sep3).join("/");
|
|
3249
4636
|
try {
|
|
3250
|
-
await
|
|
4637
|
+
await access2(absolutePath);
|
|
3251
4638
|
const content = await readFile(absolutePath);
|
|
3252
|
-
files[relativePath] =
|
|
4639
|
+
files[relativePath] = createHash5("sha256").update(content).digest("hex");
|
|
3253
4640
|
} catch {
|
|
3254
4641
|
delete files[relativePath];
|
|
3255
4642
|
}
|
|
@@ -3257,20 +4644,23 @@ async function updateSnapshot(previous, options, changedPaths) {
|
|
|
3257
4644
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
3258
4645
|
}
|
|
3259
4646
|
async function createSnapshot(options) {
|
|
3260
|
-
const rootDir =
|
|
3261
|
-
const outDir =
|
|
4647
|
+
const rootDir = resolve3(options.rootDir);
|
|
4648
|
+
const outDir = resolve3(options.outDir);
|
|
3262
4649
|
const paths = await listSourceFiles(rootDir, outDir);
|
|
3263
4650
|
const files = {};
|
|
3264
4651
|
for (const path of paths) {
|
|
3265
4652
|
const content = await readFile(path);
|
|
3266
|
-
files[
|
|
4653
|
+
files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
|
|
3267
4654
|
}
|
|
3268
4655
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
3269
4656
|
}
|
|
3270
4657
|
function optionsKeyOf(options) {
|
|
3271
4658
|
return JSON.stringify({
|
|
4659
|
+
rootDir: resolve3(options.rootDir),
|
|
4660
|
+
outDir: resolve3(options.outDir),
|
|
3272
4661
|
include: options.include,
|
|
3273
4662
|
strict: options.strict,
|
|
4663
|
+
writeOnError: options.writeOnError,
|
|
3274
4664
|
moduleBoundaryPreset: options.moduleBoundaryPreset,
|
|
3275
4665
|
moduleBoundaries: options.moduleBoundaries,
|
|
3276
4666
|
allowRouteCommandBindings: options.allowRouteCommandBindings,
|
|
@@ -3278,27 +4668,16 @@ function optionsKeyOf(options) {
|
|
|
3278
4668
|
disallowControllerDirectDb: options.disallowControllerDirectDb,
|
|
3279
4669
|
detectOrphanModules: options.detectOrphanModules,
|
|
3280
4670
|
generateClient: options.generateClient,
|
|
3281
|
-
generatePermissions: options.generatePermissions
|
|
4671
|
+
generatePermissions: options.generatePermissions,
|
|
4672
|
+
typeSafety: options.typeSafety,
|
|
4673
|
+
treeShakeUnusedProviders: options.treeShakeUnusedProviders
|
|
3282
4674
|
});
|
|
3283
4675
|
}
|
|
3284
|
-
async function requiresGraphRebuild(rootDir, changedFiles) {
|
|
3285
|
-
for (const relativePath of changedFiles) {
|
|
3286
|
-
const path = resolve(rootDir, relativePath);
|
|
3287
|
-
try {
|
|
3288
|
-
const source = await readFile(path, "utf8");
|
|
3289
|
-
if (/@(?:Module|Injectable|Inject|Controller|Command|Query)\b|new\s+InjectionToken\b|\bdefineModule\s*\(/.test(source))
|
|
3290
|
-
return true;
|
|
3291
|
-
} catch {
|
|
3292
|
-
return true;
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
return false;
|
|
3296
|
-
}
|
|
3297
4676
|
async function listSourceFiles(rootDir, outDir) {
|
|
3298
4677
|
const result = [];
|
|
3299
4678
|
const visit = async (directory) => {
|
|
3300
4679
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
3301
|
-
const path =
|
|
4680
|
+
const path = resolve3(directory, entry.name);
|
|
3302
4681
|
if (entry.isDirectory()) {
|
|
3303
4682
|
if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
|
|
3304
4683
|
continue;
|
|
@@ -3356,7 +4735,9 @@ class ModuleDependencyGraph {
|
|
|
3356
4735
|
if (!this.dependents.has(imp)) {
|
|
3357
4736
|
this.dependents.set(imp, new Set);
|
|
3358
4737
|
}
|
|
3359
|
-
this.dependents.get(imp)
|
|
4738
|
+
const dependents = this.dependents.get(imp);
|
|
4739
|
+
if (dependents)
|
|
4740
|
+
dependents.add(modName);
|
|
3360
4741
|
}
|
|
3361
4742
|
}
|
|
3362
4743
|
}
|
|
@@ -3367,7 +4748,9 @@ class ModuleDependencyGraph {
|
|
|
3367
4748
|
if (!this.fileOwners.has(normalized)) {
|
|
3368
4749
|
this.fileOwners.set(normalized, new Set);
|
|
3369
4750
|
}
|
|
3370
|
-
this.fileOwners.get(normalized)
|
|
4751
|
+
const owners = this.fileOwners.get(normalized);
|
|
4752
|
+
if (owners)
|
|
4753
|
+
owners.add(moduleName);
|
|
3371
4754
|
}
|
|
3372
4755
|
getModulesOwningFile(filePath) {
|
|
3373
4756
|
const normalized = filePath.replace(/\.(tsx?|mts|cts)$/, "");
|
|
@@ -3383,12 +4766,14 @@ class ModuleDependencyGraph {
|
|
|
3383
4766
|
}
|
|
3384
4767
|
}
|
|
3385
4768
|
if (directlyAffected.size === 0) {
|
|
3386
|
-
return
|
|
4769
|
+
return [];
|
|
3387
4770
|
}
|
|
3388
4771
|
const affected = new Set(directlyAffected);
|
|
3389
4772
|
const queue = Array.from(directlyAffected);
|
|
3390
4773
|
while (queue.length > 0) {
|
|
3391
4774
|
const current = queue.shift();
|
|
4775
|
+
if (!current)
|
|
4776
|
+
continue;
|
|
3392
4777
|
const dependents = this.dependents.get(current);
|
|
3393
4778
|
if (dependents) {
|
|
3394
4779
|
for (const dep of dependents) {
|
|
@@ -3416,8 +4801,8 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
3416
4801
|
// src/watch.ts
|
|
3417
4802
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
3418
4803
|
function watchProject(options) {
|
|
3419
|
-
const rootDir =
|
|
3420
|
-
const outDir =
|
|
4804
|
+
const rootDir = resolve4(options.rootDir);
|
|
4805
|
+
const outDir = resolve4(options.outDir);
|
|
3421
4806
|
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
3422
4807
|
let timer;
|
|
3423
4808
|
let closed = false;
|
|
@@ -3427,8 +4812,12 @@ function watchProject(options) {
|
|
|
3427
4812
|
let watcher;
|
|
3428
4813
|
const incremental = createIncrementalCompiler();
|
|
3429
4814
|
let initialEvent;
|
|
3430
|
-
let resolveReady
|
|
3431
|
-
|
|
4815
|
+
let resolveReady = () => {
|
|
4816
|
+
return;
|
|
4817
|
+
};
|
|
4818
|
+
let rejectReady = () => {
|
|
4819
|
+
return;
|
|
4820
|
+
};
|
|
3432
4821
|
const ready = new Promise((resolvePromise, rejectPromise) => {
|
|
3433
4822
|
resolveReady = resolvePromise;
|
|
3434
4823
|
rejectReady = rejectPromise;
|
|
@@ -3497,12 +4886,12 @@ function watchProject(options) {
|
|
|
3497
4886
|
watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
3498
4887
|
if (!filename)
|
|
3499
4888
|
return schedule();
|
|
3500
|
-
const changedPath =
|
|
3501
|
-
const relativePath =
|
|
4889
|
+
const changedPath = resolve4(rootDir, filename.toString());
|
|
4890
|
+
const relativePath = relative4(outDir, changedPath);
|
|
3502
4891
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
3503
4892
|
return;
|
|
3504
4893
|
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
3505
|
-
schedule(
|
|
4894
|
+
schedule(relative4(rootDir, changedPath));
|
|
3506
4895
|
});
|
|
3507
4896
|
if (initialEvent)
|
|
3508
4897
|
resolveReady(initialEvent);
|
|
@@ -3523,8 +4912,8 @@ function watchProject(options) {
|
|
|
3523
4912
|
};
|
|
3524
4913
|
}
|
|
3525
4914
|
// src/inspect.ts
|
|
3526
|
-
import { existsSync as
|
|
3527
|
-
import { join as
|
|
4915
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4916
|
+
import { join as join5 } from "node:path";
|
|
3528
4917
|
function formatGraph(graph) {
|
|
3529
4918
|
const lines = [];
|
|
3530
4919
|
for (const module of graph.modules) {
|
|
@@ -3565,13 +4954,13 @@ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
|
3565
4954
|
const checks = [
|
|
3566
4955
|
{
|
|
3567
4956
|
name: "project-root",
|
|
3568
|
-
ok:
|
|
3569
|
-
detail:
|
|
4957
|
+
ok: existsSync4(rootDir),
|
|
4958
|
+
detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
3570
4959
|
},
|
|
3571
4960
|
{
|
|
3572
4961
|
name: "tsconfig",
|
|
3573
|
-
ok:
|
|
3574
|
-
detail:
|
|
4962
|
+
ok: existsSync4(join5(rootDir, "tsconfig.json")),
|
|
4963
|
+
detail: existsSync4(join5(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
3575
4964
|
},
|
|
3576
4965
|
{
|
|
3577
4966
|
name: "modules",
|
|
@@ -3665,12 +5054,15 @@ export {
|
|
|
3665
5054
|
MODULAR_MONOLITH_RULES,
|
|
3666
5055
|
MODULE_BOUNDARY_PROFILES,
|
|
3667
5056
|
ModuleDependencyGraph,
|
|
5057
|
+
TraitCompiler,
|
|
3668
5058
|
analyzeProject,
|
|
3669
5059
|
camelName,
|
|
3670
5060
|
checkProject,
|
|
3671
5061
|
compileProject,
|
|
5062
|
+
compileTraits,
|
|
3672
5063
|
createDependencyGraphCache,
|
|
3673
5064
|
createIncrementalCompiler,
|
|
5065
|
+
createIncrementalProgramSession,
|
|
3674
5066
|
doctorProject,
|
|
3675
5067
|
explainGraph,
|
|
3676
5068
|
exportGraphDot,
|
|
@@ -3681,6 +5073,8 @@ export {
|
|
|
3681
5073
|
getModuleBoundaryProfile,
|
|
3682
5074
|
renderApplication,
|
|
3683
5075
|
resolveModuleBoundaries,
|
|
5076
|
+
scanGeneratedArtifacts,
|
|
5077
|
+
scanProductionSource,
|
|
3684
5078
|
validateGraph,
|
|
3685
5079
|
watchProject
|
|
3686
5080
|
};
|