@supacloud/compiler 0.8.0 → 0.10.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 +68 -1
- package/dist/cli.js +618 -49
- 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 +585 -88
- 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,13 +2251,301 @@ 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 {
|
|
2183
|
-
method:
|
|
2548
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2184
2549
|
path: string;
|
|
2185
2550
|
handler: string;
|
|
2186
2551
|
body?: unknown;
|
|
@@ -2251,7 +2616,7 @@ export type CompiledAspect = (
|
|
|
2251
2616
|
export interface CompiledController {
|
|
2252
2617
|
path: string;
|
|
2253
2618
|
serviceKey: string;
|
|
2254
|
-
scope:
|
|
2619
|
+
scope: "application" | "request" | "job";
|
|
2255
2620
|
routes: CompiledRoute[];
|
|
2256
2621
|
}
|
|
2257
2622
|
|
|
@@ -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;
|
|
@@ -2865,7 +3230,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2865
3230
|
switch (provider.kind) {
|
|
2866
3231
|
case "class": {
|
|
2867
3232
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
|
|
2868
|
-
const args = provider.deps.map((dep) => this.
|
|
3233
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `ConstructorParameters<typeof ${useClass}>[${index}]`)).join(", ");
|
|
2869
3234
|
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
2870
3235
|
return {
|
|
2871
3236
|
constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
|
|
@@ -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
|
|
2889
|
-
const args = provider.deps.map((dep) => this.
|
|
3253
|
+
const factory2 = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
3254
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `Parameters<typeof ${factory2}>[${index}]`)).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 {
|
|
@@ -2900,7 +3265,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2900
3265
|
}
|
|
2901
3266
|
emitController(controller, kind) {
|
|
2902
3267
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
2903
|
-
const args = controller.deps.map((dep) => this.
|
|
3268
|
+
const args = controller.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(controller, dep), `ConstructorParameters<typeof ${className}>[${index}]`)).join(", ");
|
|
2904
3269
|
const key = camelName(controller.className);
|
|
2905
3270
|
const local = this.localVar(controller.className, kind);
|
|
2906
3271
|
return {
|
|
@@ -2958,6 +3323,13 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2958
3323
|
host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
|
|
2959
3324
|
};
|
|
2960
3325
|
}
|
|
3326
|
+
typedDepExpr(token, kind, options, type) {
|
|
3327
|
+
const expression = this.depExpr(token, kind, options);
|
|
3328
|
+
const localProvider = this.module.providers.find((provider) => this.locals[kind].get(provider.token) === expression);
|
|
3329
|
+
if (expression === "undefined" || localProvider && !(localProvider.kind === "factory" && !localProvider.useFactoryName))
|
|
3330
|
+
return expression;
|
|
3331
|
+
return `${expression} as ${type}`;
|
|
3332
|
+
}
|
|
2961
3333
|
depExpr(token, kind, options = {}) {
|
|
2962
3334
|
const isOptional = options.optional ?? false;
|
|
2963
3335
|
const isSelf = options.self ?? false;
|
|
@@ -3271,6 +3643,9 @@ function renderPermissions(graph) {
|
|
|
3271
3643
|
`);
|
|
3272
3644
|
}
|
|
3273
3645
|
|
|
3646
|
+
// src/validate.ts
|
|
3647
|
+
import { dirname as dirname3, relative as relative3, sep as sep3 } from "node:path";
|
|
3648
|
+
|
|
3274
3649
|
// src/profiles.ts
|
|
3275
3650
|
var MODULAR_MONOLITH_RULES = [
|
|
3276
3651
|
{
|
|
@@ -3469,6 +3844,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3469
3844
|
"invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
|
|
3470
3845
|
"unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
|
|
3471
3846
|
"missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3847
|
+
"missing-path-param": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3472
3848
|
"duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
|
|
3473
3849
|
"missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
|
|
3474
3850
|
"unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
|
|
@@ -3479,6 +3855,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3479
3855
|
"unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
|
|
3480
3856
|
"invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
|
|
3481
3857
|
"disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3858
|
+
"invalid-body-binding": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3482
3859
|
"duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
|
|
3483
3860
|
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
3484
3861
|
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
@@ -3494,7 +3871,15 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3494
3871
|
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3495
3872
|
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3496
3873
|
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
3497
|
-
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
3874
|
+
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" },
|
|
3875
|
+
"invalid-feature-states": { code: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
|
|
3876
|
+
"duplicate-feature-transition": { code: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
|
|
3877
|
+
"invalid-feature-transition": { code: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
|
|
3878
|
+
"feature-command-unresolved": { code: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
|
|
3879
|
+
"feature-governance-drift": { code: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" },
|
|
3880
|
+
"feature-route-unresolved": { code: "SC6006", docsUrl: "https://supacloud.dev/errors/SC6006" },
|
|
3881
|
+
"feature-route-drift": { code: "SC6007", docsUrl: "https://supacloud.dev/errors/SC6007" },
|
|
3882
|
+
"invalid-feature-spec": { code: "SC6008", docsUrl: "https://supacloud.dev/errors/SC6008" }
|
|
3498
3883
|
};
|
|
3499
3884
|
function validateGraph(graph, options = false) {
|
|
3500
3885
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
@@ -3550,7 +3935,7 @@ function validateGraph(graph, options = false) {
|
|
|
3550
3935
|
}
|
|
3551
3936
|
return;
|
|
3552
3937
|
}
|
|
3553
|
-
const error = (code, message, file, line, suggestion) => {
|
|
3938
|
+
const error = (code, message, file, line, suggestion, fix) => {
|
|
3554
3939
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3555
3940
|
diagnostics.push({
|
|
3556
3941
|
severity: "error",
|
|
@@ -3560,10 +3945,11 @@ function validateGraph(graph, options = false) {
|
|
|
3560
3945
|
line,
|
|
3561
3946
|
suggestion,
|
|
3562
3947
|
errorCode: meta?.code,
|
|
3563
|
-
docsUrl: meta?.docsUrl
|
|
3948
|
+
docsUrl: meta?.docsUrl,
|
|
3949
|
+
fix
|
|
3564
3950
|
});
|
|
3565
3951
|
};
|
|
3566
|
-
const warn2 = (code, message, file, line, suggestion) => {
|
|
3952
|
+
const warn2 = (code, message, file, line, suggestion, fix) => {
|
|
3567
3953
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3568
3954
|
diagnostics.push({
|
|
3569
3955
|
severity: strict ? "error" : "warn",
|
|
@@ -3573,7 +3959,8 @@ function validateGraph(graph, options = false) {
|
|
|
3573
3959
|
line,
|
|
3574
3960
|
suggestion,
|
|
3575
3961
|
errorCode: meta?.code,
|
|
3576
|
-
docsUrl: meta?.docsUrl
|
|
3962
|
+
docsUrl: meta?.docsUrl,
|
|
3963
|
+
fix
|
|
3577
3964
|
});
|
|
3578
3965
|
};
|
|
3579
3966
|
const modulesByName = new Map;
|
|
@@ -3581,6 +3968,12 @@ function validateGraph(graph, options = false) {
|
|
|
3581
3968
|
const routesByKey = new Map;
|
|
3582
3969
|
const declaredRoutes = [];
|
|
3583
3970
|
for (const module of graph.modules) {
|
|
3971
|
+
if (module.featureSpec) {
|
|
3972
|
+
for (const diagnostic of validateFeatureSpec(module.featureSpec, module)) {
|
|
3973
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
|
|
3974
|
+
diagnostics.push({ ...diagnostic, errorCode: meta?.code, docsUrl: meta?.docsUrl });
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3584
3977
|
const previousModule = modulesByName.get(module.name);
|
|
3585
3978
|
if (previousModule) {
|
|
3586
3979
|
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
@@ -3673,7 +4066,14 @@ function validateGraph(graph, options = false) {
|
|
|
3673
4066
|
if (paramBindings.length > 0) {
|
|
3674
4067
|
for (const param of pathParams) {
|
|
3675
4068
|
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
|
|
4069
|
+
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.`, {
|
|
4070
|
+
type: "add_route_parameter_binding",
|
|
4071
|
+
targetFile: controller.file,
|
|
4072
|
+
controller: controller.className,
|
|
4073
|
+
route: route.handler,
|
|
4074
|
+
parameter: param,
|
|
4075
|
+
binding: "param"
|
|
4076
|
+
});
|
|
3677
4077
|
}
|
|
3678
4078
|
}
|
|
3679
4079
|
}
|
|
@@ -3700,10 +4100,20 @@ function validateGraph(graph, options = false) {
|
|
|
3700
4100
|
}
|
|
3701
4101
|
}
|
|
3702
4102
|
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()
|
|
4103
|
+
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().`, {
|
|
4104
|
+
type: "remove_route_body_binding",
|
|
4105
|
+
targetFile: controller.file,
|
|
4106
|
+
controller: controller.className,
|
|
4107
|
+
route: route.handler
|
|
4108
|
+
});
|
|
3704
4109
|
}
|
|
3705
4110
|
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()
|
|
4111
|
+
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().`, {
|
|
4112
|
+
type: "remove_route_body_binding",
|
|
4113
|
+
targetFile: controller.file,
|
|
4114
|
+
controller: controller.className,
|
|
4115
|
+
route: route.handler
|
|
4116
|
+
});
|
|
3707
4117
|
} else if (route.hasBodyBinding && !route.body) {
|
|
3708
4118
|
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
4119
|
} else if (route.body && !route.hasBodyBinding && !route.command) {
|
|
@@ -3844,23 +4254,51 @@ function validateGraph(graph, options = false) {
|
|
|
3844
4254
|
const owner = globalProviders.get(dep);
|
|
3845
4255
|
if (!owner)
|
|
3846
4256
|
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' })
|
|
4257
|
+
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' }).`, {
|
|
4258
|
+
type: "add_module_import",
|
|
4259
|
+
targetFile: module.file,
|
|
4260
|
+
module: owner.module.name,
|
|
4261
|
+
provider: dep,
|
|
4262
|
+
symbol: owner.module.className,
|
|
4263
|
+
targetModule: module.name,
|
|
4264
|
+
importPath: (() => {
|
|
4265
|
+
const value = relative3(dirname3(module.file), owner.module.file).replace(/\.(tsx?|mts|cts)$/, "").split(sep3).join("/");
|
|
4266
|
+
return value.startsWith(".") ? value : `./${value}`;
|
|
4267
|
+
})()
|
|
4268
|
+
});
|
|
3848
4269
|
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
3849
4270
|
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
4271
|
} 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' })
|
|
4272
|
+
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' }).`, {
|
|
4273
|
+
type: "add_provider",
|
|
4274
|
+
targetFile: module.file,
|
|
4275
|
+
token: dep,
|
|
4276
|
+
module: module.name
|
|
4277
|
+
});
|
|
3852
4278
|
}
|
|
3853
4279
|
}
|
|
3854
4280
|
continue;
|
|
3855
4281
|
}
|
|
3856
4282
|
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
|
|
4283
|
+
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.`, {
|
|
4284
|
+
type: "change_provider_scope",
|
|
4285
|
+
targetFile: provider.file,
|
|
4286
|
+
provider: provider.token,
|
|
4287
|
+
from: provider.scope,
|
|
4288
|
+
to: resolved.provider.scope
|
|
4289
|
+
});
|
|
3858
4290
|
}
|
|
3859
4291
|
}
|
|
3860
4292
|
}
|
|
3861
4293
|
for (const command of module.commands) {
|
|
3862
4294
|
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."
|
|
4295
|
+
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.", {
|
|
4296
|
+
type: "add_command_permission",
|
|
4297
|
+
targetFile: module.providers.find((provider) => provider.useClass === command.className || provider.token === command.className)?.file ?? module.file,
|
|
4298
|
+
command: command.className,
|
|
4299
|
+
module: module.name,
|
|
4300
|
+
permission: `${module.name}.${command.name}`
|
|
4301
|
+
});
|
|
3864
4302
|
}
|
|
3865
4303
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
3866
4304
|
const caps = options.commandCapabilities;
|
|
@@ -4214,8 +4652,8 @@ import { join as join4 } from "node:path";
|
|
|
4214
4652
|
|
|
4215
4653
|
// src/type-safety.ts
|
|
4216
4654
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4217
|
-
import { dirname as
|
|
4218
|
-
import * as
|
|
4655
|
+
import { dirname as dirname4, join as join3, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
|
|
4656
|
+
import * as ts5 from "@typescript/typescript6";
|
|
4219
4657
|
var DEFAULT_EXCLUDES = [
|
|
4220
4658
|
"**/*.test.ts",
|
|
4221
4659
|
"**/*.spec.ts",
|
|
@@ -4239,7 +4677,7 @@ function scanGeneratedArtifacts(artifacts, strict = true) {
|
|
|
4239
4677
|
for (const [file, content] of Object.entries(artifacts)) {
|
|
4240
4678
|
if (content === undefined)
|
|
4241
4679
|
continue;
|
|
4242
|
-
const sourceFile =
|
|
4680
|
+
const sourceFile = ts5.createSourceFile(file, content, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
4243
4681
|
for (const node of descendantsOfKind2(sourceFile, isAnyKeyword)) {
|
|
4244
4682
|
diagnostics.push(makeDiagnostic("generated-any", `生成产物 ${file} 包含 any;严格生成模式要求使用 unknown、具体接口或泛型约束。`, sourceFile, node, strict));
|
|
4245
4683
|
}
|
|
@@ -4247,30 +4685,30 @@ function scanGeneratedArtifacts(artifacts, strict = true) {
|
|
|
4247
4685
|
return diagnostics;
|
|
4248
4686
|
}
|
|
4249
4687
|
function scanProductionSource(options) {
|
|
4250
|
-
const rootDir =
|
|
4688
|
+
const rootDir = resolve3(options.rootDir);
|
|
4251
4689
|
const configPath = join3(rootDir, "tsconfig.json");
|
|
4252
4690
|
const projectConfig = existsSync2(configPath) ? readProjectConfig2(configPath) : {
|
|
4253
4691
|
options: {
|
|
4254
4692
|
strict: true,
|
|
4255
4693
|
skipLibCheck: true,
|
|
4256
|
-
target:
|
|
4257
|
-
module:
|
|
4694
|
+
target: ts5.ScriptTarget.ES2022,
|
|
4695
|
+
module: ts5.ModuleKind.ESNext
|
|
4258
4696
|
},
|
|
4259
4697
|
errors: []
|
|
4260
4698
|
};
|
|
4261
4699
|
const include = options.include ?? ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
|
|
4262
|
-
const rootNames =
|
|
4700
|
+
const rootNames = ts5.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
|
|
4263
4701
|
const compilerOptions = { ...projectConfig.options, noEmit: true };
|
|
4264
|
-
const host =
|
|
4702
|
+
const host = ts5.createCompilerHost(compilerOptions);
|
|
4265
4703
|
host.getCurrentDirectory = () => rootDir;
|
|
4266
|
-
const program =
|
|
4704
|
+
const program = ts5.createProgram(rootNames, compilerOptions, host);
|
|
4267
4705
|
const outDir = options.outDir ? normalizeRelative(rootDir, options.outDir) : undefined;
|
|
4268
4706
|
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
4269
4707
|
const sourceFiles = program.getSourceFiles().filter((sourceFile) => isProductionSource(rootDir, sourceFile, excludes, outDir));
|
|
4270
4708
|
const diagnostics = [...projectConfig.errors, ...program.getOptionsDiagnostics()].map((diagnostic) => ({
|
|
4271
4709
|
severity: "error",
|
|
4272
4710
|
code: "source-config",
|
|
4273
|
-
message:
|
|
4711
|
+
message: ts5.flattenDiagnosticMessageText(diagnostic.messageText, `
|
|
4274
4712
|
`),
|
|
4275
4713
|
file: diagnostic.file ? normalizeRelative(rootDir, diagnostic.file.fileName) : normalizeRelative(rootDir, configPath),
|
|
4276
4714
|
line: diagnostic.file && diagnostic.start !== undefined ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1 : undefined,
|
|
@@ -4287,22 +4725,22 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4287
4725
|
diagnostics.push(makeDiagnostic("source-any", "生产源码使用了显式 any;请改用 unknown、具体接口或泛型约束。", sourceFile, node, strict, rootDir));
|
|
4288
4726
|
}
|
|
4289
4727
|
for (const node of descendants(sourceFile)) {
|
|
4290
|
-
if (
|
|
4291
|
-
if (
|
|
4728
|
+
if (ts5.isAsExpression(node)) {
|
|
4729
|
+
if (ts5.isAsExpression(node.parent) || ts5.isTypeAssertionExpression(node.parent))
|
|
4292
4730
|
continue;
|
|
4293
4731
|
const assertedType = node.type.getText(sourceFile);
|
|
4294
4732
|
if (assertedType === "const")
|
|
4295
4733
|
continue;
|
|
4296
4734
|
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4297
|
-
} else if (
|
|
4298
|
-
if (
|
|
4735
|
+
} else if (ts5.isTypeAssertionExpression(node)) {
|
|
4736
|
+
if (ts5.isAsExpression(node.parent) || ts5.isTypeAssertionExpression(node.parent))
|
|
4299
4737
|
continue;
|
|
4300
4738
|
diagnostics.push(makeDiagnostic("source-type-assertion", `生产源码包含类型断言 ${node.getText(sourceFile)};请优先使用类型守卫、satisfies 或显式边界解析。`, sourceFile, node, strict, rootDir));
|
|
4301
|
-
} else if (
|
|
4739
|
+
} else if (ts5.isNonNullExpression(node)) {
|
|
4302
4740
|
diagnostics.push(makeDiagnostic("source-non-null-assertion", `生产源码包含非空断言 ${node.getText(sourceFile)};请显式处理 null/undefined。`, sourceFile, node, strict, rootDir));
|
|
4303
4741
|
}
|
|
4304
4742
|
}
|
|
4305
|
-
for (const declaration of descendantsOfKind2(sourceFile,
|
|
4743
|
+
for (const declaration of descendantsOfKind2(sourceFile, ts5.isVariableDeclaration)) {
|
|
4306
4744
|
const initializer = declaration.initializer;
|
|
4307
4745
|
if (!initializer || declaration.type)
|
|
4308
4746
|
continue;
|
|
@@ -4318,11 +4756,11 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4318
4756
|
if (isLetDeclaration(declaration) && isLiteralSyntax(initializer) && !isLiteralType(declarationType)) {
|
|
4319
4757
|
diagnostics.push(makeDiagnostic("source-implicit-widening", `变量 ${declaration.name.getText(sourceFile)} 的字面量类型从 ${checker.typeToString(initializerType, initializer)} 隐式宽化为 ${checker.typeToString(declarationType, declaration)};请补充类型或使用 const。`, sourceFile, declaration, strict, rootDir));
|
|
4320
4758
|
}
|
|
4321
|
-
if (
|
|
4759
|
+
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
4760
|
diagnostics.push(makeDiagnostic("source-implicit-widening", `常量对象 ${declaration.name.getText(sourceFile)} 的字面量属性会隐式宽化;请补充对象类型或使用 as const。`, sourceFile, declaration, strict, rootDir));
|
|
4323
4761
|
}
|
|
4324
4762
|
}
|
|
4325
|
-
for (const parameter of descendantsOfKind2(sourceFile,
|
|
4763
|
+
for (const parameter of descendantsOfKind2(sourceFile, ts5.isParameter)) {
|
|
4326
4764
|
if (parameter.type)
|
|
4327
4765
|
continue;
|
|
4328
4766
|
for (const name of bindingNames(parameter.name)) {
|
|
@@ -4333,10 +4771,10 @@ function scanSourceFile(sourceFile, checker, rootDir, diagnostics, strict) {
|
|
|
4333
4771
|
}
|
|
4334
4772
|
}
|
|
4335
4773
|
function readProjectConfig2(configPath) {
|
|
4336
|
-
const config =
|
|
4774
|
+
const config = ts5.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
|
|
4337
4775
|
if (config.error)
|
|
4338
4776
|
return { options: {}, errors: [config.error] };
|
|
4339
|
-
const parsed =
|
|
4777
|
+
const parsed = ts5.parseJsonConfigFileContent(config.config, ts5.sys, dirname4(configPath));
|
|
4340
4778
|
return { options: parsed.options, errors: parsed.errors };
|
|
4341
4779
|
}
|
|
4342
4780
|
function isProductionSource(rootDir, sourceFile, excludes, outDir) {
|
|
@@ -4356,42 +4794,42 @@ function globMatches(value, pattern) {
|
|
|
4356
4794
|
return new RegExp(`^${escaped}$`).test(value);
|
|
4357
4795
|
}
|
|
4358
4796
|
function bindingNames(name) {
|
|
4359
|
-
if (
|
|
4797
|
+
if (ts5.isIdentifier(name))
|
|
4360
4798
|
return [name];
|
|
4361
|
-
return name.elements.flatMap((element) =>
|
|
4799
|
+
return name.elements.flatMap((element) => ts5.isBindingElement(element) ? bindingNames(element.name) : []);
|
|
4362
4800
|
}
|
|
4363
4801
|
function isLiteralExpression(node) {
|
|
4364
4802
|
if (!node)
|
|
4365
4803
|
return false;
|
|
4366
4804
|
return [
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4805
|
+
ts5.SyntaxKind.StringLiteral,
|
|
4806
|
+
ts5.SyntaxKind.NumericLiteral,
|
|
4807
|
+
ts5.SyntaxKind.TrueKeyword,
|
|
4808
|
+
ts5.SyntaxKind.FalseKeyword
|
|
4371
4809
|
].includes(node.kind);
|
|
4372
4810
|
}
|
|
4373
4811
|
function isLiteralSyntax(node) {
|
|
4374
|
-
return
|
|
4812
|
+
return ts5.isStringLiteral(node) || ts5.isNumericLiteral(node) || node.kind === ts5.SyntaxKind.TrueKeyword || node.kind === ts5.SyntaxKind.FalseKeyword;
|
|
4375
4813
|
}
|
|
4376
4814
|
function isLiteralType(type) {
|
|
4377
|
-
return (type.flags & (
|
|
4815
|
+
return (type.flags & (ts5.TypeFlags.StringLiteral | ts5.TypeFlags.NumberLiteral | ts5.TypeFlags.BooleanLiteral | ts5.TypeFlags.BigIntLiteral)) !== 0;
|
|
4378
4816
|
}
|
|
4379
4817
|
function isAnyType(type) {
|
|
4380
|
-
return (type.flags &
|
|
4818
|
+
return (type.flags & ts5.TypeFlags.Any) !== 0;
|
|
4381
4819
|
}
|
|
4382
4820
|
function isLetDeclaration(declaration) {
|
|
4383
|
-
return
|
|
4821
|
+
return ts5.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts5.NodeFlags.Let) !== 0;
|
|
4384
4822
|
}
|
|
4385
4823
|
function isConstDeclaration(declaration) {
|
|
4386
|
-
return
|
|
4824
|
+
return ts5.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts5.NodeFlags.Const) !== 0;
|
|
4387
4825
|
}
|
|
4388
4826
|
function descendants(root) {
|
|
4389
4827
|
const result = [];
|
|
4390
4828
|
const visit = (node) => {
|
|
4391
4829
|
result.push(node);
|
|
4392
|
-
|
|
4830
|
+
ts5.forEachChild(node, visit);
|
|
4393
4831
|
};
|
|
4394
|
-
|
|
4832
|
+
ts5.forEachChild(root, visit);
|
|
4395
4833
|
return result;
|
|
4396
4834
|
}
|
|
4397
4835
|
function descendantsOfKind2(root, predicate) {
|
|
@@ -4399,9 +4837,9 @@ function descendantsOfKind2(root, predicate) {
|
|
|
4399
4837
|
const visit = (node) => {
|
|
4400
4838
|
if (predicate(node))
|
|
4401
4839
|
result.push(node);
|
|
4402
|
-
|
|
4840
|
+
ts5.forEachChild(node, visit);
|
|
4403
4841
|
};
|
|
4404
|
-
|
|
4842
|
+
ts5.forEachChild(root, visit);
|
|
4405
4843
|
return result;
|
|
4406
4844
|
}
|
|
4407
4845
|
function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir) {
|
|
@@ -4419,10 +4857,10 @@ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir)
|
|
|
4419
4857
|
};
|
|
4420
4858
|
}
|
|
4421
4859
|
function normalizeRelative(rootDir, filePath) {
|
|
4422
|
-
return
|
|
4860
|
+
return relative4(rootDir, filePath).split(sep4).join("/").replace(/^\.\//, "");
|
|
4423
4861
|
}
|
|
4424
4862
|
function isAnyKeyword(node) {
|
|
4425
|
-
return node.kind ===
|
|
4863
|
+
return node.kind === ts5.SyntaxKind.AnyKeyword;
|
|
4426
4864
|
}
|
|
4427
4865
|
|
|
4428
4866
|
// src/compile.ts
|
|
@@ -4571,12 +5009,12 @@ function resolveTypeSafety(options) {
|
|
|
4571
5009
|
}
|
|
4572
5010
|
// src/watch.ts
|
|
4573
5011
|
import { watch } from "node:fs";
|
|
4574
|
-
import { relative as
|
|
5012
|
+
import { relative as relative6, resolve as resolve5 } from "node:path";
|
|
4575
5013
|
|
|
4576
5014
|
// src/incremental.ts
|
|
4577
5015
|
import { createHash as createHash5 } from "node:crypto";
|
|
4578
|
-
import { access as access2, readdir, readFile } from "node:fs/promises";
|
|
4579
|
-
import { isAbsolute, relative as
|
|
5016
|
+
import { access as access2, readdir, readFile as readFile2 } from "node:fs/promises";
|
|
5017
|
+
import { isAbsolute as isAbsolute2, relative as relative5, resolve as resolve4, sep as sep5 } from "node:path";
|
|
4580
5018
|
function createDependencyGraphCache() {
|
|
4581
5019
|
return {
|
|
4582
5020
|
modules: new Map,
|
|
@@ -4650,20 +5088,20 @@ function createIncrementalCompiler() {
|
|
|
4650
5088
|
};
|
|
4651
5089
|
}
|
|
4652
5090
|
async function updateSnapshot(previous, options, changedPaths) {
|
|
4653
|
-
const rootDir =
|
|
4654
|
-
const outDir =
|
|
5091
|
+
const rootDir = resolve4(options.rootDir);
|
|
5092
|
+
const outDir = resolve4(options.outDir);
|
|
4655
5093
|
const files = { ...previous.files };
|
|
4656
5094
|
for (const changedPath of changedPaths) {
|
|
4657
|
-
const absolutePath =
|
|
4658
|
-
const relativeChangedPath =
|
|
4659
|
-
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${
|
|
5095
|
+
const absolutePath = isAbsolute2(changedPath) ? resolve4(changedPath) : resolve4(rootDir, changedPath);
|
|
5096
|
+
const relativeChangedPath = relative5(rootDir, absolutePath);
|
|
5097
|
+
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep5}`))
|
|
4660
5098
|
continue;
|
|
4661
5099
|
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
4662
5100
|
continue;
|
|
4663
|
-
const relativePath =
|
|
5101
|
+
const relativePath = relative5(rootDir, absolutePath).split(sep5).join("/");
|
|
4664
5102
|
try {
|
|
4665
5103
|
await access2(absolutePath);
|
|
4666
|
-
const content = await
|
|
5104
|
+
const content = await readFile2(absolutePath);
|
|
4667
5105
|
files[relativePath] = createHash5("sha256").update(content).digest("hex");
|
|
4668
5106
|
} catch {
|
|
4669
5107
|
delete files[relativePath];
|
|
@@ -4672,20 +5110,20 @@ async function updateSnapshot(previous, options, changedPaths) {
|
|
|
4672
5110
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
4673
5111
|
}
|
|
4674
5112
|
async function createSnapshot(options) {
|
|
4675
|
-
const rootDir =
|
|
4676
|
-
const outDir =
|
|
5113
|
+
const rootDir = resolve4(options.rootDir);
|
|
5114
|
+
const outDir = resolve4(options.outDir);
|
|
4677
5115
|
const paths = await listSourceFiles(rootDir, outDir);
|
|
4678
5116
|
const files = {};
|
|
4679
5117
|
for (const path of paths) {
|
|
4680
|
-
const content = await
|
|
4681
|
-
files[
|
|
5118
|
+
const content = await readFile2(path);
|
|
5119
|
+
files[relative5(rootDir, path).split(sep5).join("/")] = createHash5("sha256").update(content).digest("hex");
|
|
4682
5120
|
}
|
|
4683
5121
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
4684
5122
|
}
|
|
4685
5123
|
function optionsKeyOf(options) {
|
|
4686
5124
|
return JSON.stringify({
|
|
4687
|
-
rootDir:
|
|
4688
|
-
outDir:
|
|
5125
|
+
rootDir: resolve4(options.rootDir),
|
|
5126
|
+
outDir: resolve4(options.outDir),
|
|
4689
5127
|
include: options.include,
|
|
4690
5128
|
strict: options.strict,
|
|
4691
5129
|
writeOnError: options.writeOnError,
|
|
@@ -4705,7 +5143,7 @@ async function listSourceFiles(rootDir, outDir) {
|
|
|
4705
5143
|
const result = [];
|
|
4706
5144
|
const visit = async (directory) => {
|
|
4707
5145
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
4708
|
-
const path =
|
|
5146
|
+
const path = resolve4(directory, entry.name);
|
|
4709
5147
|
if (entry.isDirectory()) {
|
|
4710
5148
|
if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
|
|
4711
5149
|
continue;
|
|
@@ -4829,8 +5267,8 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
4829
5267
|
// src/watch.ts
|
|
4830
5268
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
4831
5269
|
function watchProject(options) {
|
|
4832
|
-
const rootDir =
|
|
4833
|
-
const outDir =
|
|
5270
|
+
const rootDir = resolve5(options.rootDir);
|
|
5271
|
+
const outDir = resolve5(options.outDir);
|
|
4834
5272
|
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
4835
5273
|
let timer;
|
|
4836
5274
|
let closed = false;
|
|
@@ -4914,12 +5352,12 @@ function watchProject(options) {
|
|
|
4914
5352
|
watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
|
|
4915
5353
|
if (!filename)
|
|
4916
5354
|
return schedule();
|
|
4917
|
-
const changedPath =
|
|
4918
|
-
const relativePath =
|
|
5355
|
+
const changedPath = resolve5(rootDir, filename.toString());
|
|
5356
|
+
const relativePath = relative6(outDir, changedPath);
|
|
4919
5357
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
4920
5358
|
return;
|
|
4921
5359
|
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
4922
|
-
schedule(
|
|
5360
|
+
schedule(relative6(rootDir, changedPath));
|
|
4923
5361
|
});
|
|
4924
5362
|
if (initialEvent)
|
|
4925
5363
|
resolveReady(initialEvent);
|
|
@@ -4978,6 +5416,61 @@ function explainGraph(graph, subject) {
|
|
|
4978
5416
|
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
4979
5417
|
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
4980
5418
|
}
|
|
5419
|
+
function createContextPack(graph, subject) {
|
|
5420
|
+
const subjectModule = graph.modules.find((module) => module.name === subject);
|
|
5421
|
+
if (!subjectModule) {
|
|
5422
|
+
throw new Error(`No module named "${subject}". Context packs require a module name.`);
|
|
5423
|
+
}
|
|
5424
|
+
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
5425
|
+
const selected = new Set([subjectModule.name]);
|
|
5426
|
+
const queue = [subjectModule.name];
|
|
5427
|
+
while (queue.length > 0) {
|
|
5428
|
+
const current = queue.shift();
|
|
5429
|
+
if (!current)
|
|
5430
|
+
continue;
|
|
5431
|
+
const module = byName.get(current);
|
|
5432
|
+
if (!module)
|
|
5433
|
+
continue;
|
|
5434
|
+
const neighbors = [
|
|
5435
|
+
...module.imports,
|
|
5436
|
+
...graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name)
|
|
5437
|
+
];
|
|
5438
|
+
for (const neighbor of neighbors) {
|
|
5439
|
+
if (!selected.has(neighbor) && byName.has(neighbor)) {
|
|
5440
|
+
selected.add(neighbor);
|
|
5441
|
+
queue.push(neighbor);
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
const modules = graph.modules.filter((module) => selected.has(module.name));
|
|
5446
|
+
const files = [...new Set(modules.flatMap((module) => [
|
|
5447
|
+
module.file,
|
|
5448
|
+
...module.providers.map((provider) => provider.file),
|
|
5449
|
+
...module.controllers.map((controller) => controller.file)
|
|
5450
|
+
]))].sort();
|
|
5451
|
+
const referencedTokens = new Set;
|
|
5452
|
+
for (const module of modules) {
|
|
5453
|
+
for (const provider of module.providers) {
|
|
5454
|
+
for (const token of provider.deps)
|
|
5455
|
+
referencedTokens.add(token);
|
|
5456
|
+
}
|
|
5457
|
+
for (const controller of module.controllers) {
|
|
5458
|
+
for (const token of controller.deps)
|
|
5459
|
+
referencedTokens.add(token);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
return {
|
|
5463
|
+
version: 1,
|
|
5464
|
+
subject: subjectModule.name,
|
|
5465
|
+
modules,
|
|
5466
|
+
files,
|
|
5467
|
+
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
5468
|
+
relatedModules: {
|
|
5469
|
+
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
5470
|
+
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
5471
|
+
}
|
|
5472
|
+
};
|
|
5473
|
+
}
|
|
4981
5474
|
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
4982
5475
|
const checks = [
|
|
4983
5476
|
{
|
|
@@ -5077,7 +5570,7 @@ function exportGraphDot(graph) {
|
|
|
5077
5570
|
}
|
|
5078
5571
|
// src/config.ts
|
|
5079
5572
|
import { existsSync as existsSync5 } from "node:fs";
|
|
5080
|
-
import { join as join6, resolve as
|
|
5573
|
+
import { join as join6, resolve as resolve6 } from "node:path";
|
|
5081
5574
|
import { pathToFileURL } from "node:url";
|
|
5082
5575
|
var DEFAULT_SUPACLOUD_CONFIG = {
|
|
5083
5576
|
root: "src",
|
|
@@ -5099,8 +5592,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
5099
5592
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
5100
5593
|
const resolved = defineSupacloudConfig(config);
|
|
5101
5594
|
return {
|
|
5102
|
-
rootDir:
|
|
5103
|
-
outDir:
|
|
5595
|
+
rootDir: resolve6(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
|
|
5596
|
+
outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
5104
5597
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
5105
5598
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
5106
5599
|
generateClient: resolved.generateClient ?? DEFAULT_SUPACLOUD_CONFIG.generateClient,
|
|
@@ -5147,11 +5640,13 @@ export {
|
|
|
5147
5640
|
ModuleDependencyGraph,
|
|
5148
5641
|
TraitCompiler,
|
|
5149
5642
|
analyzeProject,
|
|
5643
|
+
applyDiagnosticFix,
|
|
5150
5644
|
camelName,
|
|
5151
5645
|
checkProject,
|
|
5152
5646
|
compileOptionsFromConfig,
|
|
5153
5647
|
compileProject,
|
|
5154
5648
|
compileTraits,
|
|
5649
|
+
createContextPack,
|
|
5155
5650
|
createDependencyGraphCache,
|
|
5156
5651
|
createIncrementalCompiler,
|
|
5157
5652
|
createIncrementalProgramSession,
|
|
@@ -5162,6 +5657,7 @@ export {
|
|
|
5162
5657
|
exportGraphMermaid,
|
|
5163
5658
|
formatGraph,
|
|
5164
5659
|
generateApplication,
|
|
5660
|
+
generateFeatureSource,
|
|
5165
5661
|
getModuleBoundaryPreset,
|
|
5166
5662
|
getModuleBoundaryProfile,
|
|
5167
5663
|
loadSupacloudConfig,
|
|
@@ -5170,6 +5666,7 @@ export {
|
|
|
5170
5666
|
resolveSupacloudConfig,
|
|
5171
5667
|
scanGeneratedArtifacts,
|
|
5172
5668
|
scanProductionSource,
|
|
5669
|
+
validateFeatureSpec,
|
|
5173
5670
|
validateGraph,
|
|
5174
5671
|
watchProject
|
|
5175
5672
|
};
|