@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/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) !== "defineModule")
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: string;
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: string;
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 writeFile(temporaryPath, content, "utf8");
2481
- await rename(temporaryPath, path);
2845
+ await writeFile2(temporaryPath, content, "utf8");
2846
+ await rename2(temporaryPath, path);
2482
2847
  } catch (error) {
2483
- await unlink(temporaryPath).catch(() => {
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.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
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 factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
2889
- const args = provider.deps.map((dep) => this.depExpr(dep, kind, this.depOptions(provider, dep))).join(", ");
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} = ${factory}(${args});`, key, expr: 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.depExpr(dep, kind, this.depOptions(controller, dep))).join(", ");
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 dirname2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
4218
- import * as ts4 from "@typescript/typescript6";
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 = ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
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 = resolve2(options.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: ts4.ScriptTarget.ES2022,
4257
- module: ts4.ModuleKind.ESNext
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 = ts4.sys.readDirectory(rootDir, [".ts", ".tsx", ".mts", ".cts"], ["node_modules", "dist"], include).filter((file) => isProductionSourcePath(rootDir, file, [...DEFAULT_EXCLUDES, ...options.exclude ?? []]));
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 = ts4.createCompilerHost(compilerOptions);
4702
+ const host = ts5.createCompilerHost(compilerOptions);
4265
4703
  host.getCurrentDirectory = () => rootDir;
4266
- const program = ts4.createProgram(rootNames, compilerOptions, host);
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: ts4.flattenDiagnosticMessageText(diagnostic.messageText, `
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 (ts4.isAsExpression(node)) {
4291
- if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
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 (ts4.isTypeAssertionExpression(node)) {
4298
- if (ts4.isAsExpression(node.parent) || ts4.isTypeAssertionExpression(node.parent))
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 (ts4.isNonNullExpression(node)) {
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, ts4.isVariableDeclaration)) {
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 (ts4.isObjectLiteralExpression(initializer) && isConstDeclaration(declaration) && initializer.getText(sourceFile).length > 0 && initializer.properties.some((property) => ts4.isPropertyAssignment(property) && property.initializer !== undefined && !ts4.isAsExpression(property.initializer) && isLiteralExpression(property.initializer))) {
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, ts4.isParameter)) {
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 = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
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 = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname2(configPath));
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 (ts4.isIdentifier(name))
4797
+ if (ts5.isIdentifier(name))
4360
4798
  return [name];
4361
- return name.elements.flatMap((element) => ts4.isBindingElement(element) ? bindingNames(element.name) : []);
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
- ts4.SyntaxKind.StringLiteral,
4368
- ts4.SyntaxKind.NumericLiteral,
4369
- ts4.SyntaxKind.TrueKeyword,
4370
- ts4.SyntaxKind.FalseKeyword
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 ts4.isStringLiteral(node) || ts4.isNumericLiteral(node) || node.kind === ts4.SyntaxKind.TrueKeyword || node.kind === ts4.SyntaxKind.FalseKeyword;
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 & (ts4.TypeFlags.StringLiteral | ts4.TypeFlags.NumberLiteral | ts4.TypeFlags.BooleanLiteral | ts4.TypeFlags.BigIntLiteral)) !== 0;
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 & ts4.TypeFlags.Any) !== 0;
4818
+ return (type.flags & ts5.TypeFlags.Any) !== 0;
4381
4819
  }
4382
4820
  function isLetDeclaration(declaration) {
4383
- return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Let) !== 0;
4821
+ return ts5.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts5.NodeFlags.Let) !== 0;
4384
4822
  }
4385
4823
  function isConstDeclaration(declaration) {
4386
- return ts4.isVariableDeclarationList(declaration.parent) && (declaration.parent.flags & ts4.NodeFlags.Const) !== 0;
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
- ts4.forEachChild(node, visit);
4830
+ ts5.forEachChild(node, visit);
4393
4831
  };
4394
- ts4.forEachChild(root, visit);
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
- ts4.forEachChild(node, visit);
4840
+ ts5.forEachChild(node, visit);
4403
4841
  };
4404
- ts4.forEachChild(root, visit);
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 relative2(rootDir, filePath).split(sep2).join("/").replace(/^\.\//, "");
4860
+ return relative4(rootDir, filePath).split(sep4).join("/").replace(/^\.\//, "");
4423
4861
  }
4424
4862
  function isAnyKeyword(node) {
4425
- return node.kind === ts4.SyntaxKind.AnyKeyword;
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 relative4, resolve as resolve4 } from "node:path";
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 relative3, resolve as resolve3, sep as sep3 } from "node:path";
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 = resolve3(options.rootDir);
4654
- const outDir = resolve3(options.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 = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
4658
- const relativeChangedPath = relative3(rootDir, absolutePath);
4659
- if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep3}`))
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 = relative3(rootDir, absolutePath).split(sep3).join("/");
5101
+ const relativePath = relative5(rootDir, absolutePath).split(sep5).join("/");
4664
5102
  try {
4665
5103
  await access2(absolutePath);
4666
- const content = await readFile(absolutePath);
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 = resolve3(options.rootDir);
4676
- const outDir = resolve3(options.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 readFile(path);
4681
- files[relative3(rootDir, path).split(sep3).join("/")] = createHash5("sha256").update(content).digest("hex");
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: resolve3(options.rootDir),
4688
- outDir: resolve3(options.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 = resolve3(directory, entry.name);
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 = resolve4(options.rootDir);
4833
- const outDir = resolve4(options.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 = resolve4(rootDir, filename.toString());
4918
- const relativePath = relative4(outDir, changedPath);
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(relative4(rootDir, changedPath));
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 resolve5 } from "node:path";
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: resolve5(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
5103
- outDir: resolve5(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.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
  };