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