@supacloud/compiler 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -1
- package/dist/cli.js +608 -44
- package/dist/config.d.ts +4 -2
- package/dist/feature.d.ts +8 -0
- package/dist/fixes.d.ts +13 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +575 -83
- package/dist/inspect.d.ts +17 -1
- package/dist/traits.d.ts +1 -1
- package/dist/types.d.ts +68 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -101,6 +101,15 @@ function createDefaultTraitHandlers() {
|
|
|
101
101
|
return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineModule" ? node.name.text : undefined;
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
kind: "defineFeatureSlice",
|
|
106
|
+
detect: (node) => {
|
|
107
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
|
|
108
|
+
return;
|
|
109
|
+
const initializer = node.initializer;
|
|
110
|
+
return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineFeatureSlice" ? node.name.text : undefined;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
104
113
|
{
|
|
105
114
|
kind: "injectionToken",
|
|
106
115
|
detect: (node) => {
|
|
@@ -449,6 +458,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
449
458
|
checker,
|
|
450
459
|
tokensByName: new Map,
|
|
451
460
|
classesByName: new Map,
|
|
461
|
+
variablesByName: new Map,
|
|
452
462
|
diagnostics: []
|
|
453
463
|
};
|
|
454
464
|
const nativeTraitFiles = new Map;
|
|
@@ -483,9 +493,9 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
483
493
|
});
|
|
484
494
|
}
|
|
485
495
|
}
|
|
486
|
-
if (!cache || traits?.has("defineModule")) {
|
|
496
|
+
if (!cache || traits?.has("defineModule") || traits?.has("defineFeatureSlice")) {
|
|
487
497
|
for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
|
|
488
|
-
if (nodeText(call.expression)
|
|
498
|
+
if (!["defineModule", "defineFeatureSlice"].includes(nodeText(call.expression)))
|
|
489
499
|
continue;
|
|
490
500
|
const parent = call.parent;
|
|
491
501
|
if (!parent || !ts3.isVariableDeclaration(parent))
|
|
@@ -791,6 +801,9 @@ function indexFile(sf, ctx) {
|
|
|
791
801
|
}
|
|
792
802
|
for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
|
|
793
803
|
for (const decl of statement.declarationList.declarations) {
|
|
804
|
+
if (ts3.isIdentifier(decl.name) && !ctx.variablesByName.has(decl.name.text)) {
|
|
805
|
+
ctx.variablesByName.set(decl.name.text, decl);
|
|
806
|
+
}
|
|
794
807
|
const info = parseTokenVariable(decl, sf.fileName);
|
|
795
808
|
if (info && !ctx.tokensByName.has(info.name)) {
|
|
796
809
|
ctx.tokensByName.set(info.name, info);
|
|
@@ -828,6 +841,7 @@ function parseTokenVariable(decl, file) {
|
|
|
828
841
|
function parseModule(candidate, nameByNode, ctx) {
|
|
829
842
|
const { options, className, file, line } = candidate;
|
|
830
843
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
844
|
+
const featureSpec = parseFeatureSpec(getProp(options, "spec"), ctx);
|
|
831
845
|
const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
|
|
832
846
|
const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
|
|
833
847
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
@@ -1002,8 +1016,71 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
1002
1016
|
jobs,
|
|
1003
1017
|
queries,
|
|
1004
1018
|
...aspects.length > 0 ? { aspects } : {},
|
|
1005
|
-
exports
|
|
1019
|
+
exports,
|
|
1020
|
+
...featureSpec ? { featureSpec } : {}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
1024
|
+
if (!input)
|
|
1025
|
+
return;
|
|
1026
|
+
if (seen.has(input))
|
|
1027
|
+
return;
|
|
1028
|
+
seen.add(input);
|
|
1029
|
+
if (ts3.isIdentifier(input)) {
|
|
1030
|
+
const local = input.getSourceFile().statements.flatMap((statement) => ts3.isVariableStatement(statement) ? [...statement.declarationList.declarations] : []);
|
|
1031
|
+
const resolved = resolveDeclaration(input, ctx)[0];
|
|
1032
|
+
const decl = (resolved && ts3.isVariableDeclaration(resolved) ? resolved : undefined) ?? ctx.variablesByName.get(input.text) ?? local.find((candidate) => ts3.isIdentifier(candidate.name) && candidate.name.text === input.text) ?? descendantsOfKind(input.getSourceFile(), ts3.isVariableDeclaration).find((candidate) => ts3.isIdentifier(candidate.name) && candidate.name.text === input.text);
|
|
1033
|
+
if (decl && ts3.isVariableDeclaration(decl))
|
|
1034
|
+
return parseFeatureSpec(decl.initializer, ctx, seen);
|
|
1035
|
+
}
|
|
1036
|
+
if (ts3.isCallExpression(input) && nodeText(input.expression) === "defineFeatureSpec") {
|
|
1037
|
+
return parseFeatureSpec(input.arguments[0], ctx, seen);
|
|
1038
|
+
}
|
|
1039
|
+
if (ts3.isAsExpression(input) || ts3.isSatisfiesExpression(input) || ts3.isParenthesizedExpression(input)) {
|
|
1040
|
+
return parseFeatureSpec(input.expression, ctx, seen);
|
|
1041
|
+
}
|
|
1042
|
+
const invalid = () => {
|
|
1043
|
+
ctx.diagnostics.push({
|
|
1044
|
+
severity: "error",
|
|
1045
|
+
code: "invalid-feature-spec",
|
|
1046
|
+
message: "Feature spec must use static name, states and transition objects.",
|
|
1047
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
1048
|
+
line: lineOf(input)
|
|
1049
|
+
});
|
|
1050
|
+
return;
|
|
1006
1051
|
};
|
|
1052
|
+
if (!ts3.isObjectLiteralExpression(input))
|
|
1053
|
+
return invalid();
|
|
1054
|
+
const name = stringLiteralProp(input, "name");
|
|
1055
|
+
const statesExpr = getProp(input, "states");
|
|
1056
|
+
const transitionObject = getProp(input, "transitions");
|
|
1057
|
+
if (!name || !statesExpr || !ts3.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts3.isStringLiteral(state)) || !transitionObject || !ts3.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts3.isPropertyAssignment(property))) {
|
|
1058
|
+
return invalid();
|
|
1059
|
+
}
|
|
1060
|
+
const states = statesExpr.elements.map((state) => state.text);
|
|
1061
|
+
const transitions = [];
|
|
1062
|
+
for (const property of transitionObject.properties) {
|
|
1063
|
+
if (!ts3.isPropertyAssignment(property) || ts3.isComputedPropertyName(property.name) || !ts3.isObjectLiteralExpression(property.initializer))
|
|
1064
|
+
return invalid();
|
|
1065
|
+
const options = property.initializer;
|
|
1066
|
+
const from = stringLiteralProp(options, "from");
|
|
1067
|
+
const to = stringLiteralProp(options, "to");
|
|
1068
|
+
if (!from || !to || options.properties.some((prop) => !ts3.isPropertyAssignment(prop)) || ["permission", "command", "route", "audit"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
|
|
1069
|
+
return invalid();
|
|
1070
|
+
}
|
|
1071
|
+
transitions.push({
|
|
1072
|
+
name: propertyName(property.name),
|
|
1073
|
+
from,
|
|
1074
|
+
to,
|
|
1075
|
+
permission: stringLiteralProp(options, "permission"),
|
|
1076
|
+
command: stringLiteralProp(options, "command"),
|
|
1077
|
+
route: stringLiteralProp(options, "route"),
|
|
1078
|
+
transaction: commandModeProp(options, "transaction"),
|
|
1079
|
+
idempotency: commandModeProp(options, "idempotency"),
|
|
1080
|
+
audit: stringLiteralProp(options, "audit")
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return { name, states, transitions, file: sourcePath(ctx.rootDir, input.getSourceFile().fileName), line: lineOf(input) };
|
|
1007
1084
|
}
|
|
1008
1085
|
function commandModeProp(object, name) {
|
|
1009
1086
|
const value = stringLiteralProp(object, name);
|
|
@@ -2174,9 +2251,297 @@ function sourcePath(rootDir, absFile) {
|
|
|
2174
2251
|
function warn(ctx, code, message, file, line) {
|
|
2175
2252
|
ctx.diagnostics.push({ severity: "warn", code, message, file, line });
|
|
2176
2253
|
}
|
|
2254
|
+
// src/feature.ts
|
|
2255
|
+
function validateFeatureSpec(spec, module) {
|
|
2256
|
+
const diagnostics = [];
|
|
2257
|
+
const error = (code, message) => {
|
|
2258
|
+
diagnostics.push({ severity: "error", code, message, file: spec.file, line: spec.line });
|
|
2259
|
+
};
|
|
2260
|
+
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
2261
|
+
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
2262
|
+
}
|
|
2263
|
+
const names = new Set;
|
|
2264
|
+
for (const transition of spec.transitions) {
|
|
2265
|
+
if (!transition.name.trim() || names.has(transition.name)) {
|
|
2266
|
+
error("duplicate-feature-transition", `Feature ${spec.name} has duplicate/empty transition '${transition.name}'.`);
|
|
2267
|
+
}
|
|
2268
|
+
names.add(transition.name);
|
|
2269
|
+
if (!spec.states.includes(transition.from) || !spec.states.includes(transition.to)) {
|
|
2270
|
+
error("invalid-feature-transition", `Transition ${transition.name} references an undeclared state.`);
|
|
2271
|
+
}
|
|
2272
|
+
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
2273
|
+
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
2274
|
+
}
|
|
2275
|
+
if (!module)
|
|
2276
|
+
continue;
|
|
2277
|
+
const commands = module.commands.filter((command2) => command2.className === transition.command || command2.name === transition.command);
|
|
2278
|
+
const command = commands[0];
|
|
2279
|
+
if (transition.command && commands.length !== 1) {
|
|
2280
|
+
error("feature-command-unresolved", `Transition ${transition.name} must reference exactly one command in module ${module.name}.`);
|
|
2281
|
+
}
|
|
2282
|
+
if (!transition.command && [transition.permission, transition.transaction, transition.idempotency, transition.audit].some((value) => value !== undefined)) {
|
|
2283
|
+
error("feature-command-unresolved", `Transition ${transition.name} declares governance without a command binding.`);
|
|
2284
|
+
}
|
|
2285
|
+
if (command) {
|
|
2286
|
+
for (const key of ["permission", "transaction", "idempotency", "audit"]) {
|
|
2287
|
+
if (transition[key] !== undefined && transition[key] !== command[key]) {
|
|
2288
|
+
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (transition.route) {
|
|
2293
|
+
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
2294
|
+
if (routes.length !== 1) {
|
|
2295
|
+
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
2296
|
+
} else if (command && routes[0].command !== command.className) {
|
|
2297
|
+
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
return diagnostics;
|
|
2302
|
+
}
|
|
2303
|
+
function generateFeatureSource(spec) {
|
|
2304
|
+
const errors = validateFeatureSpec(spec);
|
|
2305
|
+
if (errors.length)
|
|
2306
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
2307
|
+
`));
|
|
2308
|
+
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
2309
|
+
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
2310
|
+
}
|
|
2311
|
+
const transitions = spec.transitions.map((transition, index) => ({
|
|
2312
|
+
...transition,
|
|
2313
|
+
command: `Transition${index + 1}Command`
|
|
2314
|
+
}));
|
|
2315
|
+
if (transitions.some((transition) => transition.route)) {
|
|
2316
|
+
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
2317
|
+
}
|
|
2318
|
+
const sourceSpec = { name: spec.name, states: spec.states, transitions: Object.fromEntries(transitions.map((transition) => {
|
|
2319
|
+
const { name, ...options } = transition;
|
|
2320
|
+
return [name, options];
|
|
2321
|
+
})) };
|
|
2322
|
+
return [
|
|
2323
|
+
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
2324
|
+
"",
|
|
2325
|
+
`export const featureSpec = defineFeatureSpec(${JSON.stringify(sourceSpec, null, 2)});`,
|
|
2326
|
+
`export type FeatureState = typeof featureSpec.states[number];`,
|
|
2327
|
+
"",
|
|
2328
|
+
...transitions.flatMap((transition) => [
|
|
2329
|
+
`@Command(${JSON.stringify({
|
|
2330
|
+
name: `${spec.name}.${transition.name}`,
|
|
2331
|
+
permission: transition.permission,
|
|
2332
|
+
transaction: transition.transaction ?? "none",
|
|
2333
|
+
idempotency: transition.idempotency ?? "none",
|
|
2334
|
+
audit: transition.audit
|
|
2335
|
+
})})`,
|
|
2336
|
+
`export class ${transition.command} {`,
|
|
2337
|
+
` execute(state: FeatureState): never {`,
|
|
2338
|
+
` if (state !== ${JSON.stringify(transition.from)}) throw new Error("Invalid transition state");`,
|
|
2339
|
+
` throw new Error(${JSON.stringify(`Implement ${spec.name}.${transition.name}: persist state ${transition.to}`)});`,
|
|
2340
|
+
" }",
|
|
2341
|
+
""
|
|
2342
|
+
]),
|
|
2343
|
+
"export const FeatureSlice = defineFeatureSlice({",
|
|
2344
|
+
` name: ${JSON.stringify(spec.name)},`,
|
|
2345
|
+
' tags: ["type:feature"],',
|
|
2346
|
+
" spec: featureSpec,",
|
|
2347
|
+
` providers: [${transitions.map((transition) => transition.command).join(", ")}],`,
|
|
2348
|
+
"});",
|
|
2349
|
+
""
|
|
2350
|
+
].join(`
|
|
2351
|
+
`);
|
|
2352
|
+
}
|
|
2353
|
+
// src/fixes.ts
|
|
2354
|
+
import { randomUUID } from "node:crypto";
|
|
2355
|
+
import { lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
2356
|
+
import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2357
|
+
import * as ts4 from "@typescript/typescript6";
|
|
2358
|
+
async function applyDiagnosticFix(fix, options = {}) {
|
|
2359
|
+
if (!fix || typeof fix.targetFile !== "string")
|
|
2360
|
+
throw new Error("Invalid DiagnosticFix");
|
|
2361
|
+
const root = await realpath(options.rootDir ?? process.cwd());
|
|
2362
|
+
const file = resolve2(root, fix.targetFile);
|
|
2363
|
+
const stat = await lstat(file);
|
|
2364
|
+
const resolved = await realpath(file);
|
|
2365
|
+
const relativePath = relative2(root, resolved);
|
|
2366
|
+
if (stat.isSymbolicLink() || !stat.isFile() || isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${sep2}`)) {
|
|
2367
|
+
throw new Error("Fix target must be a regular file inside rootDir");
|
|
2368
|
+
}
|
|
2369
|
+
const original = await readFile(file, "utf8");
|
|
2370
|
+
let source = parse(file, original);
|
|
2371
|
+
let content;
|
|
2372
|
+
switch (fix.type) {
|
|
2373
|
+
case "add_module_import": {
|
|
2374
|
+
if (!fix.importPath || !fix.symbol)
|
|
2375
|
+
throw new Error("Module fix requires importPath and symbol");
|
|
2376
|
+
identifier(fix.symbol);
|
|
2377
|
+
const withImport = importSymbol(source, fix.importPath, fix.symbol);
|
|
2378
|
+
source = parse(file, withImport);
|
|
2379
|
+
const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
|
|
2380
|
+
const imports = property(object, "imports");
|
|
2381
|
+
if (imports && !ts4.isArrayLiteralExpression(imports.initializer)) {
|
|
2382
|
+
throw new Error("Module imports must be a static array");
|
|
2383
|
+
}
|
|
2384
|
+
const values = imports && ts4.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
|
|
2385
|
+
if (values.some(ts4.isSpreadElement))
|
|
2386
|
+
throw new Error("Module imports cannot contain spread elements");
|
|
2387
|
+
content = values.some((value) => ts4.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts4.factory.createArrayLiteralExpression([
|
|
2388
|
+
...values,
|
|
2389
|
+
ts4.factory.createIdentifier(fix.symbol)
|
|
2390
|
+
]));
|
|
2391
|
+
break;
|
|
2392
|
+
}
|
|
2393
|
+
case "add_command_permission": {
|
|
2394
|
+
const permission = options.permission ?? fix.permission;
|
|
2395
|
+
if (!permission?.trim()) {
|
|
2396
|
+
throw new Error("Permission fix requires an explicit permission; privileges are never inferred");
|
|
2397
|
+
}
|
|
2398
|
+
const command = findClass(source, fix.command);
|
|
2399
|
+
const object = decoratorObject(command, "Command");
|
|
2400
|
+
const current = property(object, "permission");
|
|
2401
|
+
if (current && (!ts4.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
|
|
2402
|
+
throw new Error("Command permission already exists with a different value");
|
|
2403
|
+
}
|
|
2404
|
+
content = current ? original : replaceProperty(source, object, "permission", ts4.factory.createStringLiteral(permission));
|
|
2405
|
+
break;
|
|
2406
|
+
}
|
|
2407
|
+
case "add_route_parameter_binding": {
|
|
2408
|
+
const controller = findClass(source, fix.controller);
|
|
2409
|
+
const method = unique(controller.members.filter((member) => ts4.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
|
|
2410
|
+
const parameter = unique(method.parameters.filter((candidate) => ts4.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
|
|
2411
|
+
const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
|
|
2412
|
+
if (!binding)
|
|
2413
|
+
throw new Error("Invalid route binding");
|
|
2414
|
+
const decorators = ts4.getDecorators(parameter) ?? [];
|
|
2415
|
+
if (decorators.length > 0)
|
|
2416
|
+
throw new Error("Parameter already has a decorator");
|
|
2417
|
+
const framework = unique(source.statements.filter((statement) => ts4.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts4.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some((element) => ["Controller", "Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].includes(element.name.text))), "framework import");
|
|
2418
|
+
if (!ts4.isImportDeclaration(framework) || !ts4.isStringLiteral(framework.moduleSpecifier)) {
|
|
2419
|
+
throw new Error("Framework import must be static");
|
|
2420
|
+
}
|
|
2421
|
+
const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
|
|
2422
|
+
content = importSymbol(parse(file, edited), framework.moduleSpecifier.text, binding);
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
default:
|
|
2426
|
+
throw new Error(`Diagnostic fix '${fix.type}' requires a manual semantic decision; no files changed`);
|
|
2427
|
+
}
|
|
2428
|
+
parse(file, content);
|
|
2429
|
+
const result = { file, changed: content !== original, content };
|
|
2430
|
+
if (options.dryRun === false && result.changed) {
|
|
2431
|
+
const temporary = `${file}.supacloud-fix-${randomUUID()}`;
|
|
2432
|
+
try {
|
|
2433
|
+
await writeFile(temporary, content, { encoding: "utf8", flag: "wx", mode: stat.mode });
|
|
2434
|
+
if (await readFile(file, "utf8") !== original)
|
|
2435
|
+
throw new Error("Target changed while preparing the fix");
|
|
2436
|
+
await rename(temporary, file);
|
|
2437
|
+
} finally {
|
|
2438
|
+
await unlink(temporary).catch(() => {});
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return result;
|
|
2442
|
+
}
|
|
2443
|
+
function parse(file, text) {
|
|
2444
|
+
const result = ts4.transpileModule(text, {
|
|
2445
|
+
fileName: file,
|
|
2446
|
+
reportDiagnostics: true,
|
|
2447
|
+
compilerOptions: { target: ts4.ScriptTarget.ESNext, experimentalDecorators: true }
|
|
2448
|
+
});
|
|
2449
|
+
if (result.diagnostics?.some((item) => item.category === ts4.DiagnosticCategory.Error)) {
|
|
2450
|
+
throw new Error("Cannot fix syntactically invalid TypeScript");
|
|
2451
|
+
}
|
|
2452
|
+
return ts4.createSourceFile(file, text, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
2453
|
+
}
|
|
2454
|
+
function unique(items, description) {
|
|
2455
|
+
if (items.length !== 1)
|
|
2456
|
+
throw new Error(`Expected exactly one ${description}; found ${items.length}`);
|
|
2457
|
+
return items[0];
|
|
2458
|
+
}
|
|
2459
|
+
function identifier(value) {
|
|
2460
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(value))
|
|
2461
|
+
throw new Error(`Invalid identifier '${value}'`);
|
|
2462
|
+
}
|
|
2463
|
+
function nameOf(name) {
|
|
2464
|
+
if (!name)
|
|
2465
|
+
return "";
|
|
2466
|
+
return ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name) ? name.text : "";
|
|
2467
|
+
}
|
|
2468
|
+
function property(object, key) {
|
|
2469
|
+
if (object.properties.some((item) => !ts4.isPropertyAssignment(item) || ts4.isComputedPropertyName(item.name))) {
|
|
2470
|
+
throw new Error("Fix requires explicit static object properties");
|
|
2471
|
+
}
|
|
2472
|
+
const values = object.properties.filter((item) => ts4.isPropertyAssignment(item) && nameOf(item.name) === key);
|
|
2473
|
+
if (values.length > 1)
|
|
2474
|
+
throw new Error(`Duplicate '${key}' property`);
|
|
2475
|
+
return values[0];
|
|
2476
|
+
}
|
|
2477
|
+
function stringProperty(object, key) {
|
|
2478
|
+
const value = property(object, key)?.initializer;
|
|
2479
|
+
return value && ts4.isStringLiteral(value) ? value.text : undefined;
|
|
2480
|
+
}
|
|
2481
|
+
function replaceProperty(source, object, key, value) {
|
|
2482
|
+
const previous = property(object, key);
|
|
2483
|
+
const replacement = ts4.factory.createPropertyAssignment(key, value);
|
|
2484
|
+
const properties = object.properties.map((item) => item === previous ? replacement : item);
|
|
2485
|
+
if (!previous)
|
|
2486
|
+
properties.push(replacement);
|
|
2487
|
+
const updated = ts4.factory.updateObjectLiteralExpression(object, properties);
|
|
2488
|
+
return source.text.slice(0, object.getStart(source)) + ts4.createPrinter().printNode(ts4.EmitHint.Expression, updated, source) + source.text.slice(object.end);
|
|
2489
|
+
}
|
|
2490
|
+
function findClass(source, name) {
|
|
2491
|
+
return unique(source.statements.filter((statement) => ts4.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
|
|
2492
|
+
}
|
|
2493
|
+
function decoratorObject(node, name) {
|
|
2494
|
+
const decorator = unique((ts4.getDecorators(node) ?? []).filter((item) => ts4.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
|
|
2495
|
+
const argument = ts4.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
|
|
2496
|
+
if (!argument || !ts4.isObjectLiteralExpression(argument))
|
|
2497
|
+
throw new Error(`@${name} requires a static object`);
|
|
2498
|
+
return argument;
|
|
2499
|
+
}
|
|
2500
|
+
function moduleObjects(source) {
|
|
2501
|
+
const result = [];
|
|
2502
|
+
for (const statement of source.statements) {
|
|
2503
|
+
if (ts4.isClassDeclaration(statement) && (ts4.getDecorators(statement) ?? []).some((item) => ts4.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
|
|
2504
|
+
result.push(decoratorObject(statement, "Module"));
|
|
2505
|
+
}
|
|
2506
|
+
if (ts4.isVariableStatement(statement)) {
|
|
2507
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2508
|
+
const call = declaration.initializer;
|
|
2509
|
+
if (call && ts4.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts4.isObjectLiteralExpression(call.arguments[0])) {
|
|
2510
|
+
result.push(call.arguments[0]);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return result;
|
|
2516
|
+
}
|
|
2517
|
+
function importSymbol(source, path, symbol) {
|
|
2518
|
+
identifier(symbol);
|
|
2519
|
+
const current = resolve2(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
|
|
2520
|
+
const target = resolve2(dirname2(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
|
|
2521
|
+
if (current === target)
|
|
2522
|
+
return source.text;
|
|
2523
|
+
const matches = source.statements.filter((item) => ts4.isImportDeclaration(item) && ts4.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
|
|
2524
|
+
if (matches.length > 1)
|
|
2525
|
+
throw new Error(`Ambiguous imports from '${path}'`);
|
|
2526
|
+
const match = matches[0];
|
|
2527
|
+
if (match && ts4.isImportDeclaration(match) && match.importClause?.namedBindings && ts4.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
|
|
2528
|
+
if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
|
|
2529
|
+
return source.text;
|
|
2530
|
+
const bindings = match.importClause.namedBindings;
|
|
2531
|
+
const updated = ts4.factory.updateNamedImports(bindings, [
|
|
2532
|
+
...bindings.elements,
|
|
2533
|
+
ts4.factory.createImportSpecifier(false, undefined, ts4.factory.createIdentifier(symbol))
|
|
2534
|
+
]);
|
|
2535
|
+
return source.text.slice(0, bindings.getStart(source)) + ts4.createPrinter().printNode(ts4.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
|
|
2536
|
+
}
|
|
2537
|
+
if (match)
|
|
2538
|
+
throw new Error(`Import from '${path}' is not a named value import`);
|
|
2539
|
+
return `import { ${symbol} } from ${JSON.stringify(path)};
|
|
2540
|
+
${source.text}`;
|
|
2541
|
+
}
|
|
2177
2542
|
// src/generate.ts
|
|
2178
2543
|
import { createHash as createHash4 } from "node:crypto";
|
|
2179
|
-
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
2544
|
+
import { access, mkdir, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2180
2545
|
import { join as join2 } from "node:path";
|
|
2181
2546
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2182
2547
|
var INTERFACES = `export interface CompiledRoute {
|
|
@@ -2477,10 +2842,10 @@ async function writeFileIfChanged(path, content, hashes) {
|
|
|
2477
2842
|
async function writeFileAtomic(path, content) {
|
|
2478
2843
|
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
2479
2844
|
try {
|
|
2480
|
-
await
|
|
2481
|
-
await
|
|
2845
|
+
await writeFile2(temporaryPath, content, "utf8");
|
|
2846
|
+
await rename2(temporaryPath, path);
|
|
2482
2847
|
} catch (error) {
|
|
2483
|
-
await
|
|
2848
|
+
await unlink2(temporaryPath).catch(() => {
|
|
2484
2849
|
return;
|
|
2485
2850
|
});
|
|
2486
2851
|
throw error;
|
|
@@ -2885,10 +3250,10 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2885
3250
|
const constLine = `const ${local2} = resolveFactoryValue(${tokenIdent});`;
|
|
2886
3251
|
return { constLine, key, expr: local2 };
|
|
2887
3252
|
}
|
|
2888
|
-
const
|
|
3253
|
+
const factory2 = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
2889
3254
|
const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
|
|
2890
3255
|
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
2891
|
-
return { constLine: `const ${local} = ${
|
|
3256
|
+
return { constLine: `const ${local} = ${factory2}(${args});`, key, expr: local };
|
|
2892
3257
|
}
|
|
2893
3258
|
case "existing": {
|
|
2894
3259
|
return {
|
|
@@ -3271,6 +3636,9 @@ function renderPermissions(graph) {
|
|
|
3271
3636
|
`);
|
|
3272
3637
|
}
|
|
3273
3638
|
|
|
3639
|
+
// src/validate.ts
|
|
3640
|
+
import { dirname as dirname3, relative as relative3, sep as sep3 } from "node:path";
|
|
3641
|
+
|
|
3274
3642
|
// src/profiles.ts
|
|
3275
3643
|
var MODULAR_MONOLITH_RULES = [
|
|
3276
3644
|
{
|
|
@@ -3469,6 +3837,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3469
3837
|
"invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
|
|
3470
3838
|
"unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
|
|
3471
3839
|
"missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3840
|
+
"missing-path-param": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3472
3841
|
"duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
|
|
3473
3842
|
"missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
|
|
3474
3843
|
"unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
|
|
@@ -3479,6 +3848,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3479
3848
|
"unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
|
|
3480
3849
|
"invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
|
|
3481
3850
|
"disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3851
|
+
"invalid-body-binding": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3482
3852
|
"duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
|
|
3483
3853
|
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
3484
3854
|
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
@@ -3494,7 +3864,15 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3494
3864
|
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3495
3865
|
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3496
3866
|
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
3497
|
-
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
3867
|
+
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" },
|
|
3868
|
+
"invalid-feature-states": { code: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
|
|
3869
|
+
"duplicate-feature-transition": { code: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
|
|
3870
|
+
"invalid-feature-transition": { code: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
|
|
3871
|
+
"feature-command-unresolved": { code: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
|
|
3872
|
+
"feature-governance-drift": { code: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" },
|
|
3873
|
+
"feature-route-unresolved": { code: "SC6006", docsUrl: "https://supacloud.dev/errors/SC6006" },
|
|
3874
|
+
"feature-route-drift": { code: "SC6007", docsUrl: "https://supacloud.dev/errors/SC6007" },
|
|
3875
|
+
"invalid-feature-spec": { code: "SC6008", docsUrl: "https://supacloud.dev/errors/SC6008" }
|
|
3498
3876
|
};
|
|
3499
3877
|
function validateGraph(graph, options = false) {
|
|
3500
3878
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
@@ -3550,7 +3928,7 @@ function validateGraph(graph, options = false) {
|
|
|
3550
3928
|
}
|
|
3551
3929
|
return;
|
|
3552
3930
|
}
|
|
3553
|
-
const error = (code, message, file, line, suggestion) => {
|
|
3931
|
+
const error = (code, message, file, line, suggestion, fix) => {
|
|
3554
3932
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3555
3933
|
diagnostics.push({
|
|
3556
3934
|
severity: "error",
|
|
@@ -3560,10 +3938,11 @@ function validateGraph(graph, options = false) {
|
|
|
3560
3938
|
line,
|
|
3561
3939
|
suggestion,
|
|
3562
3940
|
errorCode: meta?.code,
|
|
3563
|
-
docsUrl: meta?.docsUrl
|
|
3941
|
+
docsUrl: meta?.docsUrl,
|
|
3942
|
+
fix
|
|
3564
3943
|
});
|
|
3565
3944
|
};
|
|
3566
|
-
const warn2 = (code, message, file, line, suggestion) => {
|
|
3945
|
+
const warn2 = (code, message, file, line, suggestion, fix) => {
|
|
3567
3946
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3568
3947
|
diagnostics.push({
|
|
3569
3948
|
severity: strict ? "error" : "warn",
|
|
@@ -3573,7 +3952,8 @@ function validateGraph(graph, options = false) {
|
|
|
3573
3952
|
line,
|
|
3574
3953
|
suggestion,
|
|
3575
3954
|
errorCode: meta?.code,
|
|
3576
|
-
docsUrl: meta?.docsUrl
|
|
3955
|
+
docsUrl: meta?.docsUrl,
|
|
3956
|
+
fix
|
|
3577
3957
|
});
|
|
3578
3958
|
};
|
|
3579
3959
|
const modulesByName = new Map;
|
|
@@ -3581,6 +3961,12 @@ function validateGraph(graph, options = false) {
|
|
|
3581
3961
|
const routesByKey = new Map;
|
|
3582
3962
|
const declaredRoutes = [];
|
|
3583
3963
|
for (const module of graph.modules) {
|
|
3964
|
+
if (module.featureSpec) {
|
|
3965
|
+
for (const diagnostic of validateFeatureSpec(module.featureSpec, module)) {
|
|
3966
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
|
|
3967
|
+
diagnostics.push({ ...diagnostic, errorCode: meta?.code, docsUrl: meta?.docsUrl });
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3584
3970
|
const previousModule = modulesByName.get(module.name);
|
|
3585
3971
|
if (previousModule) {
|
|
3586
3972
|
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
@@ -3673,7 +4059,14 @@ function validateGraph(graph, options = false) {
|
|
|
3673
4059
|
if (paramBindings.length > 0) {
|
|
3674
4060
|
for (const param of pathParams) {
|
|
3675
4061
|
if (!paramBindings.includes(param)) {
|
|
3676
|
-
warn2("missing-path-param", `Route path '${route.path}' defines parameter ':${param}', but handler ${controller.className}.${route.handler} does not bind it with @Param('${param}').`, controller.file, undefined, `Add @Param('${param}') to ${route.handler} arguments
|
|
4062
|
+
warn2("missing-path-param", `Route path '${route.path}' defines parameter ':${param}', but handler ${controller.className}.${route.handler} does not bind it with @Param('${param}').`, controller.file, undefined, `Add @Param('${param}') to ${route.handler} arguments.`, {
|
|
4063
|
+
type: "add_route_parameter_binding",
|
|
4064
|
+
targetFile: controller.file,
|
|
4065
|
+
controller: controller.className,
|
|
4066
|
+
route: route.handler,
|
|
4067
|
+
parameter: param,
|
|
4068
|
+
binding: "param"
|
|
4069
|
+
});
|
|
3677
4070
|
}
|
|
3678
4071
|
}
|
|
3679
4072
|
}
|
|
@@ -3700,10 +4093,20 @@ function validateGraph(graph, options = false) {
|
|
|
3700
4093
|
}
|
|
3701
4094
|
}
|
|
3702
4095
|
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
|
|
3703
|
-
error("disallowed-body-on-get-delete", `Route handler ${controller.className}.${route.handler} binds @Body() or declares body schema on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param()
|
|
4096
|
+
error("disallowed-body-on-get-delete", `Route handler ${controller.className}.${route.handler} binds @Body() or declares body schema on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`, {
|
|
4097
|
+
type: "remove_route_body_binding",
|
|
4098
|
+
targetFile: controller.file,
|
|
4099
|
+
controller: controller.className,
|
|
4100
|
+
route: route.handler
|
|
4101
|
+
});
|
|
3704
4102
|
}
|
|
3705
4103
|
if (route.hasBodyBinding && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
3706
|
-
error("invalid-body-binding", `Route handler ${controller.className}.${route.handler} binds @Body() on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param()
|
|
4104
|
+
error("invalid-body-binding", `Route handler ${controller.className}.${route.handler} binds @Body() on HTTP ${route.method} route '${route.path}'. Request bodies are not supported on ${route.method} requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for routes accepting a request body, or bind parameters via @Query() / @Param().`, {
|
|
4105
|
+
type: "remove_route_body_binding",
|
|
4106
|
+
targetFile: controller.file,
|
|
4107
|
+
controller: controller.className,
|
|
4108
|
+
route: route.handler
|
|
4109
|
+
});
|
|
3707
4110
|
} else if (route.hasBodyBinding && !route.body) {
|
|
3708
4111
|
warn2("missing-body-schema", `Route handler ${controller.className}.${route.handler} binds @Body() on route '${route.path}', but route definition does not specify a body validation schema.`, controller.file, undefined, `Add schema to route options (e.g. body: Schema) for compile-time and runtime validation.`);
|
|
3709
4112
|
} else if (route.body && !route.hasBodyBinding && !route.command) {
|
|
@@ -3844,23 +4247,51 @@ function validateGraph(graph, options = false) {
|
|
|
3844
4247
|
const owner = globalProviders.get(dep);
|
|
3845
4248
|
if (!owner)
|
|
3846
4249
|
continue;
|
|
3847
|
-
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' })
|
|
4250
|
+
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' }).`, {
|
|
4251
|
+
type: "add_module_import",
|
|
4252
|
+
targetFile: module.file,
|
|
4253
|
+
module: owner.module.name,
|
|
4254
|
+
provider: dep,
|
|
4255
|
+
symbol: owner.module.className,
|
|
4256
|
+
targetModule: module.name,
|
|
4257
|
+
importPath: (() => {
|
|
4258
|
+
const value = relative3(dirname3(module.file), owner.module.file).replace(/\.(tsx?|mts|cts)$/, "").split(sep3).join("/");
|
|
4259
|
+
return value.startsWith(".") ? value : `./${value}`;
|
|
4260
|
+
})()
|
|
4261
|
+
});
|
|
3848
4262
|
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
3849
4263
|
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: () => ... }).`);
|
|
3850
4264
|
} else {
|
|
3851
|
-
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line, `Provide '${dep}' in a module, mark constructor parameter @Optional(), or define @Injectable({ providedIn: 'root' })
|
|
4265
|
+
error("unresolved-token", `模块 ${module.name} 的 provider ${provider.token} 依赖的 token ${dep} 无法解析`, provider.file, provider.line, `Provide '${dep}' in a module, mark constructor parameter @Optional(), or define @Injectable({ providedIn: 'root' }).`, {
|
|
4266
|
+
type: "add_provider",
|
|
4267
|
+
targetFile: module.file,
|
|
4268
|
+
token: dep,
|
|
4269
|
+
module: module.name
|
|
4270
|
+
});
|
|
3852
4271
|
}
|
|
3853
4272
|
}
|
|
3854
4273
|
continue;
|
|
3855
4274
|
}
|
|
3856
4275
|
if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
|
|
3857
|
-
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line, `Change provider '${provider.token}' scope to '${resolved.provider.scope}', or inject a factory/context instead
|
|
4276
|
+
error("scope-violation", `模块 ${module.name} 的 ${provider.scope} provider ${provider.token} 不能依赖 ${resolved.provider.scope} provider ${dep}`, provider.file, provider.line, `Change provider '${provider.token}' scope to '${resolved.provider.scope}', or inject a factory/context instead.`, {
|
|
4277
|
+
type: "change_provider_scope",
|
|
4278
|
+
targetFile: provider.file,
|
|
4279
|
+
provider: provider.token,
|
|
4280
|
+
from: provider.scope,
|
|
4281
|
+
to: resolved.provider.scope
|
|
4282
|
+
});
|
|
3858
4283
|
}
|
|
3859
4284
|
}
|
|
3860
4285
|
}
|
|
3861
4286
|
for (const command of module.commands) {
|
|
3862
4287
|
if (!command.permission) {
|
|
3863
|
-
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line, "Add 'permission: string' to @Command({ ... }) or configure command execution capabilities permission=false."
|
|
4288
|
+
error("command-missing-permission", `模块 ${module.name} 的 command ${command.name} (${command.className}) 未声明 permission`, module.file, module.line, "Add 'permission: string' to @Command({ ... }) or configure command execution capabilities permission=false.", {
|
|
4289
|
+
type: "add_command_permission",
|
|
4290
|
+
targetFile: module.providers.find((provider) => provider.useClass === command.className || provider.token === command.className)?.file ?? module.file,
|
|
4291
|
+
command: command.className,
|
|
4292
|
+
module: module.name,
|
|
4293
|
+
permission: `${module.name}.${command.name}`
|
|
4294
|
+
});
|
|
3864
4295
|
}
|
|
3865
4296
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
3866
4297
|
const caps = options.commandCapabilities;
|
|
@@ -4214,8 +4645,8 @@ import { join as join4 } from "node:path";
|
|
|
4214
4645
|
|
|
4215
4646
|
// src/type-safety.ts
|
|
4216
4647
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4217
|
-
import { dirname as
|
|
4218
|
-
import * as
|
|
4648
|
+
import { dirname as dirname4, join as join3, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
|
|
4649
|
+
import * as ts5 from "@typescript/typescript6";
|
|
4219
4650
|
var DEFAULT_EXCLUDES = [
|
|
4220
4651
|
"**/*.test.ts",
|
|
4221
4652
|
"**/*.spec.ts",
|
|
@@ -4239,7 +4670,7 @@ function scanGeneratedArtifacts(artifacts, strict = true) {
|
|
|
4239
4670
|
for (const [file, content] of Object.entries(artifacts)) {
|
|
4240
4671
|
if (content === undefined)
|
|
4241
4672
|
continue;
|
|
4242
|
-
const sourceFile =
|
|
4673
|
+
const sourceFile = ts5.createSourceFile(file, content, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
4243
4674
|
for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
|
|
4244
4675
|
diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
|
|
4245
4676
|
}
|
|
@@ -4247,30 +4678,30 @@ function scanGeneratedArtifacts(artifacts, strict = true) {
|
|
|
4247
4678
|
return diagnostics;
|
|
4248
4679
|
}
|
|
4249
4680
|
function scanProductionSource(options) {
|
|
4250
|
-
const rootDir =
|
|
4681
|
+
const rootDir = resolve3(options.rootDir);
|
|
4251
4682
|
const configPath = join3(rootDir, "tsconfig.json");
|
|
4252
4683
|
const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
|
|
4253
4684
|
options: {
|
|
4254
4685
|
strict: true,
|
|
4255
4686
|
skipLibCheck: true,
|
|
4256
|
-
target:
|
|
4257
|
-
module:
|
|
4687
|
+
target: ts5.ScriptTarget.ES2022,
|
|
4688
|
+
module: ts5.ModuleKind.ESNext
|
|
4258
4689
|
},
|
|
4259
4690
|
errors: []
|
|
4260
4691
|
};
|
|
4261
4692
|
const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
|
|
4262
|
-
const rootNames =
|
|
4693
|
+
const rootNames = ts5.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
|
|
4263
4694
|
const compilerOptions = { ...projectConfig.options, noEmit: true };
|
|
4264
|
-
const host =
|
|
4695
|
+
const host = ts5.createCompilerHost(compilerOptions);
|
|
4265
4696
|
host.getCurrentDirectory = () => rootDir;
|
|
4266
|
-
const program =
|
|
4697
|
+
const program = ts5.createProgram(rootNames, compilerOptions, host);
|
|
4267
4698
|
const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
|
|
4268
4699
|
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
4269
4700
|
const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
|
|
4270
4701
|
const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
|
|
4271
4702
|
severity: "error",
|
|
4272
4703
|
code: "source-config",
|
|
4273
|
-
message:
|
|
4704
|
+
message: ts5.flattenDiagnosticMessageText(diagnostic.messageText, `
|
|
4274
4705
|
`),
|
|
4275
4706
|
file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
|
|
4276
4707
|
line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
|
|
@@ -4287,22 +4718,22 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4287
4718
|
diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
|
|
4288
4719
|
}
|
|
4289
4720
|
for (const node of descendants(sourceFile)) {
|
|
4290
|
-
if (
|
|
4291
|
-
if (
|
|
4721
|
+
if (ts5.isAsExpression(node)) {
|
|
4722
|
+
if (ts5.isAsExpression(node.parent) || ts5.isTypeAssertionExpression(node.parent))
|
|
4292
4723
|
continue;
|
|
4293
4724
|
const assertedType = node.type.getText(sourceFile);
|
|
4294
4725
|
if (assertedType === "const")
|
|
4295
4726
|
continue;
|
|
4296
4727
|
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4297
|
-
} else if (
|
|
4298
|
-
if (
|
|
4728
|
+
} else if (ts5.isTypeAssertionExpression(node)) {
|
|
4729
|
+
if (ts5.isAsExpression(node.parent) || ts5.isTypeAssertionExpression(node.parent))
|
|
4299
4730
|
continue;
|
|
4300
4731
|
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4301
|
-
} else if (
|
|
4732
|
+
} else if (ts5.isNonNullExpression(node)) {
|
|
4302
4733
|
diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
|
|
4303
4734
|
}
|
|
4304
4735
|
}
|
|
4305
|
-
for (const declaration of descendantsOfKind2(sourceFile,
|
|
4736
|
+
for (const declaration of descendantsOfKind2(sourceFile, ts5.isVariableDeclaration)) {
|
|
4306
4737
|
const initializer = declaration.initializer;
|
|
4307
4738
|
if (!initializer || declaration.type)
|
|
4308
4739
|
continue;
|
|
@@ -4318,11 +4749,11 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4318
4749
|
if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
|
|
4319
4750
|
diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
|
|
4320
4751
|
}
|
|
4321
|
-
if (
|
|
4752
|
+
if (ts5.isObjectLiteralExpression(initializer) && isConstDeclaration(declaration) && initializer.getText(sourceFile).length > 0 && initializer.properties.some((property2) => ts5.isPropertyAssignment(property2) && property2.initializer !== undefined && !ts5.isAsExpression(property2.initializer) && isLiteralExpression(property2.initializer))) {
|
|
4322
4753
|
diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
|
|
4323
4754
|
}
|
|
4324
4755
|
}
|
|
4325
|
-
for (const parameter of descendantsOfKind2(sourceFile,
|
|
4756
|
+
for (const parameter of descendantsOfKind2(sourceFile, ts5.isParameter)) {
|
|
4326
4757
|
if (parameter.type)
|
|
4327
4758
|
continue;
|
|
4328
4759
|
for (const name of bindingNames(parameter.name)) {
|
|
@@ -4333,10 +4764,10 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4333
4764
|
}
|
|
4334
4765
|
}
|
|
4335
4766
|
function readProjectConfig2(configPath) {
|
|
4336
|
-
const config =
|
|
4767
|
+
const config = ts5.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
|
|
4337
4768
|
if (config.error)
|
|
4338
4769
|
return { options: {}, errors: [config.error] };
|
|
4339
|
-
const parsed =
|
|
4770
|
+
const parsed = ts5.parseJsonConfigFileContent(config.config, ts5.sys, dirname4(configPath));
|
|
4340
4771
|
return { options: parsed.options, errors: parsed.errors };
|
|
4341
4772
|
}
|
|
4342
4773
|
function isProductionSource(rootDir, sourceFile, excludes, outDir) {
|
|
@@ -4356,42 +4787,42 @@ function globMatches(value, pattern) {
|
|
|
4356
4787
|
return new RegExp(`^${escaped}$`).test(value);
|
|
4357
4788
|
}
|
|
4358
4789
|
function bindingNames(name) {
|
|
4359
|
-
if (
|
|
4790
|
+
if (ts5.isIdentifier(name))
|
|
4360
4791
|
return [name];
|
|
4361
|
-
return name.elements.flatMap((element) =>
|
|
4792
|
+
return name.elements.flatMap((element) => ts5.isBindingElement(element) ? bindingNames(element.name) : []);
|
|
4362
4793
|
}
|
|
4363
4794
|
function isLiteralExpression(node) {
|
|
4364
4795
|
if (!node)
|
|
4365
4796
|
return false;
|
|
4366
4797
|
return [
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4798
|
+
ts5.SyntaxKind.StringLiteral,
|
|
4799
|
+
ts5.SyntaxKind.NumericLiteral,
|
|
4800
|
+
ts5.SyntaxKind.TrueKeyword,
|
|
4801
|
+
ts5.SyntaxKind.FalseKeyword
|
|
4371
4802
|
].includes(node.kind);
|
|
4372
4803
|
}
|
|
4373
4804
|
function isLiteralSyntax(node) {
|
|
4374
|
-
return
|
|
4805
|
+
return ts5.isStringLiteral(node) || ts5.isNumericLiteral(node) || node.kind === ts5.SyntaxKind.TrueKeyword || node.kind === ts5.SyntaxKind.FalseKeyword;
|
|
4375
4806
|
}
|
|
4376
4807
|
function isLiteralType(type) {
|
|
4377
|
-
return (type.flags & (
|
|
4808
|
+
return (type.flags & (ts5.TypeFlags.StringLiteral | ts5.TypeFlags.NumberLiteral | ts5.TypeFlags.BooleanLiteral | ts5.TypeFlags.BigIntLiteral)) !== 0;
|
|
4378
4809
|
}
|
|
4379
4810
|
function isAnyType(type) {
|
|
4380
|
-
return (type.flags &
|
|
4811
|
+
return (type.flags & ts5.TypeFlags.Any) !== 0;
|
|
4381
4812
|
}
|
|
4382
4813
|
function isLetDeclaration(declaration) {
|
|
4383
|
-
return
|
|
4814
|
+
return ts5.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts5.NodeFlags.Let) !== 0;
|
|
4384
4815
|
}
|
|
4385
4816
|
function isConstDeclaration(declaration) {
|
|
4386
|
-
return
|
|
4817
|
+
return ts5.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts5.NodeFlags.Const) !== 0;
|
|
4387
4818
|
}
|
|
4388
4819
|
function descendants(root) {
|
|
4389
4820
|
const result = [];
|
|
4390
4821
|
const visit = (node) => {
|
|
4391
4822
|
result.push(node);
|
|
4392
|
-
|
|
4823
|
+
ts5.forEachChild(node, visit);
|
|
4393
4824
|
};
|
|
4394
|
-
|
|
4825
|
+
ts5.forEachChild(root, visit);
|
|
4395
4826
|
return result;
|
|
4396
4827
|
}
|
|
4397
4828
|
function descendantsOfKind2(root, predicate) {
|
|
@@ -4399,9 +4830,9 @@ function descendantsOfKind2(root, predicate) {
|
|
|
4399
4830
|
const visit = (node) => {
|
|
4400
4831
|
if (predicate(node))
|
|
4401
4832
|
result.push(node);
|
|
4402
|
-
|
|
4833
|
+
ts5.forEachChild(node, visit);
|
|
4403
4834
|
};
|
|
4404
|
-
|
|
4835
|
+
ts5.forEachChild(root, visit);
|
|
4405
4836
|
return result;
|
|
4406
4837
|
}
|
|
4407
4838
|
function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
|
|
@@ -4419,10 +4850,10 @@ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir)
|
|
|
4419
4850
|
};
|
|
4420
4851
|
}
|
|
4421
4852
|
function normalizeRelative(rootDir, filePath) {
|
|
4422
|
-
return
|
|
4853
|
+
return relative4(rootDir, filePath).split(sep4).join("/").replace(/^\.\//, "");
|
|
4423
4854
|
}
|
|
4424
4855
|
function isAnyKeyword(node) {
|
|
4425
|
-
return node.kind ===
|
|
4856
|
+
return node.kind === ts5.SyntaxKind.AnyKeyword;
|
|
4426
4857
|
}
|
|
4427
4858
|
|
|
4428
4859
|
// src/compile.ts
|
|
@@ -4571,12 +5002,12 @@ function resolveTypeSafety(options) {
|
|
|
4571
5002
|
}
|
|
4572
5003
|
// src/watch.ts
|
|
4573
5004
|
import { watch } from "node:fs";
|
|
4574
|
-
import { relative as
|
|
5005
|
+
import { relative as relative6, resolve as resolve5 } from "node:path";
|
|
4575
5006
|
|
|
4576
5007
|
// src/incremental.ts
|
|
4577
5008
|
import { createHash as createHash5 } from "node:crypto";
|
|
4578
|
-
import { access as access2, readdir, readFile } from "node:fs/promises";
|
|
4579
|
-
import { isAbsolute, relative as
|
|
5009
|
+
import { access as access2, readdir, readFile as readFile2 } from "node:fs/promises";
|
|
5010
|
+
import { isAbsolute as isAbsolute2, relative as relative5, resolve as resolve4, sep as sep5 } from "node:path";
|
|
4580
5011
|
function createDependencyGraphCache() {
|
|
4581
5012
|
return {
|
|
4582
5013
|
modules: new Map,
|
|
@@ -4650,20 +5081,20 @@ function createIncrementalCompiler() {
|
|
|
4650
5081
|
};
|
|
4651
5082
|
}
|
|
4652
5083
|
async function updateSnapshot(previous, options, changedPaths) {
|
|
4653
|
-
const rootDir =
|
|
4654
|
-
const outDir =
|
|
5084
|
+
const rootDir = resolve4(options.rootDir);
|
|
5085
|
+
const outDir = resolve4(options.outDir);
|
|
4655
5086
|
const files = { ...previous.files };
|
|
4656
5087
|
for (const changedPath of changedPaths) {
|
|
4657
|
-
const absolutePath =
|
|
4658
|
-
const relativeChangedPath =
|
|
4659
|
-
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${
|
|
5088
|
+
const absolutePath = isAbsolute2(changedPath) ? resolve4(changedPath) : resolve4(rootDir, changedPath);
|
|
5089
|
+
const relativeChangedPath = relative5(rootDir, absolutePath);
|
|
5090
|
+
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep5}`))
|
|
4660
5091
|
continue;
|
|
4661
5092
|
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
4662
5093
|
continue;
|
|
4663
|
-
const relativePath =
|
|
5094
|
+
const relativePath = relative5(rootDir, absolutePath).split(sep5).join("/");
|
|
4664
5095
|
try {
|
|
4665
5096
|
await access2(absolutePath);
|
|
4666
|
-
const content = await
|
|
5097
|
+
const content = await readFile2(absolutePath);
|
|
4667
5098
|
files[relativePath] = createHash5("sha256").update(content).digest("hex");
|
|
4668
5099
|
} catch {
|
|
4669
5100
|
delete files[relativePath];
|
|
@@ -4672,20 +5103,20 @@ async function updateSnapshot(previous, options, changedPaths) {
|
|
|
4672
5103
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
4673
5104
|
}
|
|
4674
5105
|
async function createSnapshot(options) {
|
|
4675
|
-
const rootDir =
|
|
4676
|
-
const outDir =
|
|
5106
|
+
const rootDir = resolve4(options.rootDir);
|
|
5107
|
+
const outDir = resolve4(options.outDir);
|
|
4677
5108
|
const paths = await listSourceFiles(rootDir, outDir);
|
|
4678
5109
|
const files = {};
|
|
4679
5110
|
for (const path of paths) {
|
|
4680
|
-
const content = await
|
|
4681
|
-
files[
|
|
5111
|
+
const content = await readFile2(path);
|
|
5112
|
+
files[relative5(rootDir, path).split(sep5).join("/")] = createHash5("sha256").update(content).digest("hex");
|
|
4682
5113
|
}
|
|
4683
5114
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
4684
5115
|
}
|
|
4685
5116
|
function optionsKeyOf(options) {
|
|
4686
5117
|
return JSON.stringify({
|
|
4687
|
-
rootDir:
|
|
4688
|
-
outDir:
|
|
5118
|
+
rootDir: resolve4(options.rootDir),
|
|
5119
|
+
outDir: resolve4(options.outDir),
|
|
4689
5120
|
include: options.include,
|
|
4690
5121
|
strict: options.strict,
|
|
4691
5122
|
writeOnError: options.writeOnError,
|
|
@@ -4705,7 +5136,7 @@ async function listSourceFiles(rootDir, outDir) {
|
|
|
4705
5136
|
const result = [];
|
|
4706
5137
|
const visit = async (directory) => {
|
|
4707
5138
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
4708
|
-
const path =
|
|
5139
|
+
const path = resolve4(directory, entry.name);
|
|
4709
5140
|
if (entry.isDirectory()) {
|
|
4710
5141
|
if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
|
|
4711
5142
|
continue;
|
|
@@ -4829,8 +5260,8 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
4829
5260
|
// src/watch.ts
|
|
4830
5261
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
4831
5262
|
function watchProject(options) {
|
|
4832
|
-
const rootDir =
|
|
4833
|
-
const outDir =
|
|
5263
|
+
const rootDir = resolve5(options.rootDir);
|
|
5264
|
+
const outDir = resolve5(options.outDir);
|
|
4834
5265
|
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
4835
5266
|
let timer;
|
|
4836
5267
|
let closed = false;
|
|
@@ -4914,12 +5345,12 @@ function watchProject(options) {
|
|
|
4914
5345
|
watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
4915
5346
|
if (!filename)
|
|
4916
5347
|
return schedule();
|
|
4917
|
-
const changedPath =
|
|
4918
|
-
const relativePath =
|
|
5348
|
+
const changedPath = resolve5(rootDir, filename.toString());
|
|
5349
|
+
const relativePath = relative6(outDir, changedPath);
|
|
4919
5350
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
4920
5351
|
return;
|
|
4921
5352
|
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
4922
|
-
schedule(
|
|
5353
|
+
schedule(relative6(rootDir, changedPath));
|
|
4923
5354
|
});
|
|
4924
5355
|
if (initialEvent)
|
|
4925
5356
|
resolveReady(initialEvent);
|
|
@@ -4978,6 +5409,61 @@ function explainGraph(graph, subject) {
|
|
|
4978
5409
|
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
4979
5410
|
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
4980
5411
|
}
|
|
5412
|
+
function createContextPack(graph, subject) {
|
|
5413
|
+
const subjectModule = graph.modules.find((module) => module.name === subject);
|
|
5414
|
+
if (!subjectModule) {
|
|
5415
|
+
throw new Error(`No module named "${subject}". Context packs require a module name.`);
|
|
5416
|
+
}
|
|
5417
|
+
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
5418
|
+
const selected = new Set([subjectModule.name]);
|
|
5419
|
+
const queue = [subjectModule.name];
|
|
5420
|
+
while (queue.length > 0) {
|
|
5421
|
+
const current = queue.shift();
|
|
5422
|
+
if (!current)
|
|
5423
|
+
continue;
|
|
5424
|
+
const module = byName.get(current);
|
|
5425
|
+
if (!module)
|
|
5426
|
+
continue;
|
|
5427
|
+
const neighbors = [
|
|
5428
|
+
...module.imports,
|
|
5429
|
+
...graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name)
|
|
5430
|
+
];
|
|
5431
|
+
for (const neighbor of neighbors) {
|
|
5432
|
+
if (!selected.has(neighbor) && byName.has(neighbor)) {
|
|
5433
|
+
selected.add(neighbor);
|
|
5434
|
+
queue.push(neighbor);
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
const modules = graph.modules.filter((module) => selected.has(module.name));
|
|
5439
|
+
const files = [...new Set(modules.flatMap((module) => [
|
|
5440
|
+
module.file,
|
|
5441
|
+
...module.providers.map((provider) => provider.file),
|
|
5442
|
+
...module.controllers.map((controller) => controller.file)
|
|
5443
|
+
]))].sort();
|
|
5444
|
+
const referencedTokens = new Set;
|
|
5445
|
+
for (const module of modules) {
|
|
5446
|
+
for (const provider of module.providers) {
|
|
5447
|
+
for (const token of provider.deps)
|
|
5448
|
+
referencedTokens.add(token);
|
|
5449
|
+
}
|
|
5450
|
+
for (const controller of module.controllers) {
|
|
5451
|
+
for (const token of controller.deps)
|
|
5452
|
+
referencedTokens.add(token);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
return {
|
|
5456
|
+
version: 1,
|
|
5457
|
+
subject: subjectModule.name,
|
|
5458
|
+
modules,
|
|
5459
|
+
files,
|
|
5460
|
+
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
5461
|
+
relatedModules: {
|
|
5462
|
+
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
5463
|
+
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
5464
|
+
}
|
|
5465
|
+
};
|
|
5466
|
+
}
|
|
4981
5467
|
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
4982
5468
|
const checks = [
|
|
4983
5469
|
{
|
|
@@ -5077,7 +5563,7 @@ function exportGraphDot(graph) {
|
|
|
5077
5563
|
}
|
|
5078
5564
|
// src/config.ts
|
|
5079
5565
|
import { existsSync as existsSync5 } from "node:fs";
|
|
5080
|
-
import { join as join6, resolve as
|
|
5566
|
+
import { join as join6, resolve as resolve6 } from "node:path";
|
|
5081
5567
|
import { pathToFileURL } from "node:url";
|
|
5082
5568
|
var DEFAULT_SUPACLOUD_CONFIG = {
|
|
5083
5569
|
root: "src",
|
|
@@ -5099,13 +5585,14 @@ function defineSupacloudConfig(config = {}) {
|
|
|
5099
5585
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
5100
5586
|
const resolved = defineSupacloudConfig(config);
|
|
5101
5587
|
return {
|
|
5102
|
-
rootDir:
|
|
5103
|
-
outDir:
|
|
5588
|
+
rootDir: resolve6(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
|
|
5589
|
+
outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
5104
5590
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
5105
5591
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
5106
5592
|
generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
|
|
5107
5593
|
generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
|
|
5108
5594
|
moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
|
|
5595
|
+
commandCapabilities: resolved.commandCapabilities,
|
|
5109
5596
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders
|
|
5110
5597
|
};
|
|
5111
5598
|
}
|
|
@@ -5132,6 +5619,7 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
|
|
|
5132
5619
|
generateClient: resolved.generateClient,
|
|
5133
5620
|
generatePermissions: resolved.generatePermissions,
|
|
5134
5621
|
moduleBoundaryPreset: resolved.moduleBoundaryPreset,
|
|
5622
|
+
commandCapabilities: resolved.commandCapabilities,
|
|
5135
5623
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders
|
|
5136
5624
|
};
|
|
5137
5625
|
}
|
|
@@ -5145,11 +5633,13 @@ export {
|
|
|
5145
5633
|
ModuleDependencyGraph,
|
|
5146
5634
|
TraitCompiler,
|
|
5147
5635
|
analyzeProject,
|
|
5636
|
+
applyDiagnosticFix,
|
|
5148
5637
|
camelName,
|
|
5149
5638
|
checkProject,
|
|
5150
5639
|
compileOptionsFromConfig,
|
|
5151
5640
|
compileProject,
|
|
5152
5641
|
compileTraits,
|
|
5642
|
+
createContextPack,
|
|
5153
5643
|
createDependencyGraphCache,
|
|
5154
5644
|
createIncrementalCompiler,
|
|
5155
5645
|
createIncrementalProgramSession,
|
|
@@ -5160,6 +5650,7 @@ export {
|
|
|
5160
5650
|
exportGraphMermaid,
|
|
5161
5651
|
formatGraph,
|
|
5162
5652
|
generateApplication,
|
|
5653
|
+
generateFeatureSource,
|
|
5163
5654
|
getModuleBoundaryPreset,
|
|
5164
5655
|
getModuleBoundaryProfile,
|
|
5165
5656
|
loadSupacloudConfig,
|
|
@@ -5168,6 +5659,7 @@ export {
|
|
|
5168
5659
|
resolveSupacloudConfig,
|
|
5169
5660
|
scanGeneratedArtifacts,
|
|
5170
5661
|
scanProductionSource,
|
|
5662
|
+
validateFeatureSpec,
|
|
5171
5663
|
validateGraph,
|
|
5172
5664
|
watchProject
|
|
5173
5665
|
};
|