@supacloud/compiler 0.8.0 → 0.9.0

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