@supacloud/compiler 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/dist/cli.js +618 -49
- package/dist/feature.d.ts +8 -0
- package/dist/fixes.d.ts +13 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +585 -88
- package/dist/inspect.d.ts +17 -1
- package/dist/traits.d.ts +1 -1
- package/dist/types.d.ts +68 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as
|
|
4
|
+
import { resolve as resolve7 } from "node:path";
|
|
5
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
5
6
|
|
|
6
7
|
// src/analyze.ts
|
|
7
8
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -106,6 +107,15 @@ function createDefaultTraitHandlers() {
|
|
|
106
107
|
return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineModule" ? node.name.text : undefined;
|
|
107
108
|
}
|
|
108
109
|
},
|
|
110
|
+
{
|
|
111
|
+
kind: "defineFeatureSlice",
|
|
112
|
+
detect: (node) => {
|
|
113
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name))
|
|
114
|
+
return;
|
|
115
|
+
const initializer = node.initializer;
|
|
116
|
+
return initializer && ts.isCallExpression(initializer) && expressionName(initializer.expression) === "defineFeatureSlice" ? node.name.text : undefined;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
109
119
|
{
|
|
110
120
|
kind: "injectionToken",
|
|
111
121
|
detect: (node) => {
|
|
@@ -454,6 +464,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
454
464
|
checker,
|
|
455
465
|
tokensByName: new Map,
|
|
456
466
|
classesByName: new Map,
|
|
467
|
+
variablesByName: new Map,
|
|
457
468
|
diagnostics: []
|
|
458
469
|
};
|
|
459
470
|
const nativeTraitFiles = new Map;
|
|
@@ -488,9 +499,9 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
488
499
|
});
|
|
489
500
|
}
|
|
490
501
|
}
|
|
491
|
-
if (!cache || traits?.has("defineModule")) {
|
|
502
|
+
if (!cache || traits?.has("defineModule") || traits?.has("defineFeatureSlice")) {
|
|
492
503
|
for (const call of descendantsOfKind(sf, ts3.isCallExpression)) {
|
|
493
|
-
if (nodeText(call.expression)
|
|
504
|
+
if (!["defineModule", "defineFeatureSlice"].includes(nodeText(call.expression)))
|
|
494
505
|
continue;
|
|
495
506
|
const parent = call.parent;
|
|
496
507
|
if (!parent || !ts3.isVariableDeclaration(parent))
|
|
@@ -796,6 +807,9 @@ function indexFile(sf, ctx) {
|
|
|
796
807
|
}
|
|
797
808
|
for (const statement of sf.statements.filter(ts3.isVariableStatement)) {
|
|
798
809
|
for (const decl of statement.declarationList.declarations) {
|
|
810
|
+
if (ts3.isIdentifier(decl.name) && !ctx.variablesByName.has(decl.name.text)) {
|
|
811
|
+
ctx.variablesByName.set(decl.name.text, decl);
|
|
812
|
+
}
|
|
799
813
|
const info = parseTokenVariable(decl, sf.fileName);
|
|
800
814
|
if (info && !ctx.tokensByName.has(info.name)) {
|
|
801
815
|
ctx.tokensByName.set(info.name, info);
|
|
@@ -833,6 +847,7 @@ function parseTokenVariable(decl, file) {
|
|
|
833
847
|
function parseModule(candidate, nameByNode, ctx) {
|
|
834
848
|
const { options, className, file, line } = candidate;
|
|
835
849
|
const name = nameByNode.get(candidate.node) ?? className;
|
|
850
|
+
const featureSpec = parseFeatureSpec(getProp(options, "spec"), ctx);
|
|
836
851
|
const tags = arrayProp(options, "tags").map((el) => ts3.isStringLiteral(el) ? el.text : nodeText(el).replace(/['"]/g, "")).filter(Boolean);
|
|
837
852
|
const aspects = parseAspectRefs(getProp(options, "aspects"), ctx, `module ${name}`);
|
|
838
853
|
const imports = arrayProp(options, "imports").map((el) => {
|
|
@@ -1007,8 +1022,71 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
1007
1022
|
jobs,
|
|
1008
1023
|
queries,
|
|
1009
1024
|
...aspects.length > 0 ? { aspects } : {},
|
|
1010
|
-
exports
|
|
1025
|
+
exports,
|
|
1026
|
+
...featureSpec ? { featureSpec } : {}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
1030
|
+
if (!input)
|
|
1031
|
+
return;
|
|
1032
|
+
if (seen.has(input))
|
|
1033
|
+
return;
|
|
1034
|
+
seen.add(input);
|
|
1035
|
+
if (ts3.isIdentifier(input)) {
|
|
1036
|
+
const local = input.getSourceFile().statements.flatMap((statement) => ts3.isVariableStatement(statement) ? [...statement.declarationList.declarations] : []);
|
|
1037
|
+
const resolved = resolveDeclaration(input, ctx)[0];
|
|
1038
|
+
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);
|
|
1039
|
+
if (decl && ts3.isVariableDeclaration(decl))
|
|
1040
|
+
return parseFeatureSpec(decl.initializer, ctx, seen);
|
|
1041
|
+
}
|
|
1042
|
+
if (ts3.isCallExpression(input) && nodeText(input.expression) === "defineFeatureSpec") {
|
|
1043
|
+
return parseFeatureSpec(input.arguments[0], ctx, seen);
|
|
1044
|
+
}
|
|
1045
|
+
if (ts3.isAsExpression(input) || ts3.isSatisfiesExpression(input) || ts3.isParenthesizedExpression(input)) {
|
|
1046
|
+
return parseFeatureSpec(input.expression, ctx, seen);
|
|
1047
|
+
}
|
|
1048
|
+
const invalid = () => {
|
|
1049
|
+
ctx.diagnostics.push({
|
|
1050
|
+
severity: "error",
|
|
1051
|
+
code: "invalid-feature-spec",
|
|
1052
|
+
message: "Feature spec must use static name, states and transition objects.",
|
|
1053
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
1054
|
+
line: lineOf(input)
|
|
1055
|
+
});
|
|
1056
|
+
return;
|
|
1011
1057
|
};
|
|
1058
|
+
if (!ts3.isObjectLiteralExpression(input))
|
|
1059
|
+
return invalid();
|
|
1060
|
+
const name = stringLiteralProp(input, "name");
|
|
1061
|
+
const statesExpr = getProp(input, "states");
|
|
1062
|
+
const transitionObject = getProp(input, "transitions");
|
|
1063
|
+
if (!name || !statesExpr || !ts3.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts3.isStringLiteral(state)) || !transitionObject || !ts3.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts3.isPropertyAssignment(property))) {
|
|
1064
|
+
return invalid();
|
|
1065
|
+
}
|
|
1066
|
+
const states = statesExpr.elements.map((state) => state.text);
|
|
1067
|
+
const transitions = [];
|
|
1068
|
+
for (const property of transitionObject.properties) {
|
|
1069
|
+
if (!ts3.isPropertyAssignment(property) || ts3.isComputedPropertyName(property.name) || !ts3.isObjectLiteralExpression(property.initializer))
|
|
1070
|
+
return invalid();
|
|
1071
|
+
const options = property.initializer;
|
|
1072
|
+
const from = stringLiteralProp(options, "from");
|
|
1073
|
+
const to = stringLiteralProp(options, "to");
|
|
1074
|
+
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))) {
|
|
1075
|
+
return invalid();
|
|
1076
|
+
}
|
|
1077
|
+
transitions.push({
|
|
1078
|
+
name: propertyName(property.name),
|
|
1079
|
+
from,
|
|
1080
|
+
to,
|
|
1081
|
+
permission: stringLiteralProp(options, "permission"),
|
|
1082
|
+
command: stringLiteralProp(options, "command"),
|
|
1083
|
+
route: stringLiteralProp(options, "route"),
|
|
1084
|
+
transaction: commandModeProp(options, "transaction"),
|
|
1085
|
+
idempotency: commandModeProp(options, "idempotency"),
|
|
1086
|
+
audit: stringLiteralProp(options, "audit")
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
return { name, states, transitions, file: sourcePath(ctx.rootDir, input.getSourceFile().fileName), line: lineOf(input) };
|
|
1012
1090
|
}
|
|
1013
1091
|
function commandModeProp(object, name) {
|
|
1014
1092
|
const value = stringLiteralProp(object, name);
|
|
@@ -2186,7 +2264,7 @@ import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
|
2186
2264
|
import { join as join2 } from "node:path";
|
|
2187
2265
|
var HEADER = "// GENERATED BY @supacloud/compiler — do not edit";
|
|
2188
2266
|
var INTERFACES = `export interface CompiledRoute {
|
|
2189
|
-
method:
|
|
2267
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
2190
2268
|
path: string;
|
|
2191
2269
|
handler: string;
|
|
2192
2270
|
body?: unknown;
|
|
@@ -2257,7 +2335,7 @@ export type CompiledAspect = (
|
|
|
2257
2335
|
export interface CompiledController {
|
|
2258
2336
|
path: string;
|
|
2259
2337
|
serviceKey: string;
|
|
2260
|
-
scope:
|
|
2338
|
+
scope: "application" | "request" | "job";
|
|
2261
2339
|
routes: CompiledRoute[];
|
|
2262
2340
|
}
|
|
2263
2341
|
|
|
@@ -2871,7 +2949,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2871
2949
|
switch (provider.kind) {
|
|
2872
2950
|
case "class": {
|
|
2873
2951
|
const useClass = this.imports.add(provider.useClass ?? provider.token, provider.importPath, provider.importModule);
|
|
2874
|
-
const args = provider.deps.map((dep) => this.
|
|
2952
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `ConstructorParameters<typeof ${useClass}>[${index}]`)).join(", ");
|
|
2875
2953
|
const local = this.localVar(isMulti ? provider.useClass ?? `${provider.token}Item` : provider.token, kind);
|
|
2876
2954
|
return {
|
|
2877
2955
|
constLine: `const ${local} = ${this.instantiate(useClass, args, kind, provider.functionalInjects)};`,
|
|
@@ -2892,7 +2970,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2892
2970
|
return { constLine, key, expr: local2 };
|
|
2893
2971
|
}
|
|
2894
2972
|
const factory = this.imports.add(provider.useFactoryName ?? "", provider.importPath, provider.importModule);
|
|
2895
|
-
const args = provider.deps.map((dep) => this.
|
|
2973
|
+
const args = provider.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(provider, dep), `Parameters<typeof ${factory}>[${index}]`)).join(", ");
|
|
2896
2974
|
const local = this.localVar(isMulti ? provider.useFactoryName ?? `${provider.token}Item` : provider.token, kind);
|
|
2897
2975
|
return { constLine: `const ${local} = ${factory}(${args});`, key, expr: local };
|
|
2898
2976
|
}
|
|
@@ -2906,7 +2984,7 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2906
2984
|
}
|
|
2907
2985
|
emitController(controller, kind) {
|
|
2908
2986
|
const className = this.imports.add(controller.className, controller.importPath);
|
|
2909
|
-
const args = controller.deps.map((dep) => this.
|
|
2987
|
+
const args = controller.deps.map((dep, index) => this.typedDepExpr(dep, kind, this.depOptions(controller, dep), `ConstructorParameters<typeof ${className}>[${index}]`)).join(", ");
|
|
2910
2988
|
const key = camelName(controller.className);
|
|
2911
2989
|
const local = this.localVar(controller.className, kind);
|
|
2912
2990
|
return {
|
|
@@ -2964,6 +3042,13 @@ ${indent(item, 2)}`).join(",")}
|
|
|
2964
3042
|
host: "hostDeps" in node ? node.hostDeps?.includes(token) ?? false : false
|
|
2965
3043
|
};
|
|
2966
3044
|
}
|
|
3045
|
+
typedDepExpr(token, kind, options, type) {
|
|
3046
|
+
const expression = this.depExpr(token, kind, options);
|
|
3047
|
+
const localProvider = this.module.providers.find((provider) => this.locals[kind].get(provider.token) === expression);
|
|
3048
|
+
if (expression === "undefined" || localProvider && !(localProvider.kind === "factory" && !localProvider.useFactoryName))
|
|
3049
|
+
return expression;
|
|
3050
|
+
return `${expression} as ${type}`;
|
|
3051
|
+
}
|
|
2967
3052
|
depExpr(token, kind, options = {}) {
|
|
2968
3053
|
const isOptional = options.optional ?? false;
|
|
2969
3054
|
const isSelf = options.self ?? false;
|
|
@@ -3277,6 +3362,9 @@ function renderPermissions(graph) {
|
|
|
3277
3362
|
`);
|
|
3278
3363
|
}
|
|
3279
3364
|
|
|
3365
|
+
// src/validate.ts
|
|
3366
|
+
import { dirname as dirname2, relative as relative2, sep as sep2 } from "node:path";
|
|
3367
|
+
|
|
3280
3368
|
// src/profiles.ts
|
|
3281
3369
|
var MODULAR_MONOLITH_RULES = [
|
|
3282
3370
|
{
|
|
@@ -3444,6 +3532,106 @@ function resolveModuleBoundaries(options) {
|
|
|
3444
3532
|
return merged.length > 0 ? merged : undefined;
|
|
3445
3533
|
}
|
|
3446
3534
|
|
|
3535
|
+
// src/feature.ts
|
|
3536
|
+
function validateFeatureSpec(spec, module) {
|
|
3537
|
+
const diagnostics = [];
|
|
3538
|
+
const error = (code, message) => {
|
|
3539
|
+
diagnostics.push({ severity: "error", code, message, file: spec.file, line: spec.line });
|
|
3540
|
+
};
|
|
3541
|
+
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
3542
|
+
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
3543
|
+
}
|
|
3544
|
+
const names = new Set;
|
|
3545
|
+
for (const transition of spec.transitions) {
|
|
3546
|
+
if (!transition.name.trim() || names.has(transition.name)) {
|
|
3547
|
+
error("duplicate-feature-transition", `Feature ${spec.name} has duplicate/empty transition '${transition.name}'.`);
|
|
3548
|
+
}
|
|
3549
|
+
names.add(transition.name);
|
|
3550
|
+
if (!spec.states.includes(transition.from) || !spec.states.includes(transition.to)) {
|
|
3551
|
+
error("invalid-feature-transition", `Transition ${transition.name} references an undeclared state.`);
|
|
3552
|
+
}
|
|
3553
|
+
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
3554
|
+
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
3555
|
+
}
|
|
3556
|
+
if (!module)
|
|
3557
|
+
continue;
|
|
3558
|
+
const commands = module.commands.filter((command2) => command2.className === transition.command || command2.name === transition.command);
|
|
3559
|
+
const command = commands[0];
|
|
3560
|
+
if (transition.command && commands.length !== 1) {
|
|
3561
|
+
error("feature-command-unresolved", `Transition ${transition.name} must reference exactly one command in module ${module.name}.`);
|
|
3562
|
+
}
|
|
3563
|
+
if (!transition.command && [transition.permission, transition.transaction, transition.idempotency, transition.audit].some((value) => value !== undefined)) {
|
|
3564
|
+
error("feature-command-unresolved", `Transition ${transition.name} declares governance without a command binding.`);
|
|
3565
|
+
}
|
|
3566
|
+
if (command) {
|
|
3567
|
+
for (const key of ["permission", "transaction", "idempotency", "audit"]) {
|
|
3568
|
+
if (transition[key] !== undefined && transition[key] !== command[key]) {
|
|
3569
|
+
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
if (transition.route) {
|
|
3574
|
+
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
3575
|
+
if (routes.length !== 1) {
|
|
3576
|
+
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
3577
|
+
} else if (command && routes[0].command !== command.className) {
|
|
3578
|
+
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return diagnostics;
|
|
3583
|
+
}
|
|
3584
|
+
function generateFeatureSource(spec) {
|
|
3585
|
+
const errors = validateFeatureSpec(spec);
|
|
3586
|
+
if (errors.length)
|
|
3587
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
3588
|
+
`));
|
|
3589
|
+
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
3590
|
+
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
3591
|
+
}
|
|
3592
|
+
const transitions = spec.transitions.map((transition, index) => ({
|
|
3593
|
+
...transition,
|
|
3594
|
+
command: `Transition${index + 1}Command`
|
|
3595
|
+
}));
|
|
3596
|
+
if (transitions.some((transition) => transition.route)) {
|
|
3597
|
+
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
3598
|
+
}
|
|
3599
|
+
const sourceSpec = { name: spec.name, states: spec.states, transitions: Object.fromEntries(transitions.map((transition) => {
|
|
3600
|
+
const { name, ...options } = transition;
|
|
3601
|
+
return [name, options];
|
|
3602
|
+
})) };
|
|
3603
|
+
return [
|
|
3604
|
+
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
3605
|
+
"",
|
|
3606
|
+
`export const featureSpec = defineFeatureSpec(${JSON.stringify(sourceSpec, null, 2)});`,
|
|
3607
|
+
`export type FeatureState = typeof featureSpec.states[number];`,
|
|
3608
|
+
"",
|
|
3609
|
+
...transitions.flatMap((transition) => [
|
|
3610
|
+
`@Command(${JSON.stringify({
|
|
3611
|
+
name: `${spec.name}.${transition.name}`,
|
|
3612
|
+
permission: transition.permission,
|
|
3613
|
+
transaction: transition.transaction ?? "none",
|
|
3614
|
+
idempotency: transition.idempotency ?? "none",
|
|
3615
|
+
audit: transition.audit
|
|
3616
|
+
})})`,
|
|
3617
|
+
`export class ${transition.command} {`,
|
|
3618
|
+
` execute(state: FeatureState): never {`,
|
|
3619
|
+
` if (state !== ${JSON.stringify(transition.from)}) throw new Error("Invalid transition state");`,
|
|
3620
|
+
` throw new Error(${JSON.stringify(`Implement ${spec.name}.${transition.name}: persist state ${transition.to}`)});`,
|
|
3621
|
+
" }",
|
|
3622
|
+
""
|
|
3623
|
+
]),
|
|
3624
|
+
"export const FeatureSlice = defineFeatureSlice({",
|
|
3625
|
+
` name: ${JSON.stringify(spec.name)},`,
|
|
3626
|
+
' tags: ["type:feature"],',
|
|
3627
|
+
" spec: featureSpec,",
|
|
3628
|
+
` providers: [${transitions.map((transition) => transition.command).join(", ")}],`,
|
|
3629
|
+
"});",
|
|
3630
|
+
""
|
|
3631
|
+
].join(`
|
|
3632
|
+
`);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3447
3635
|
// src/validate.ts
|
|
3448
3636
|
var SCOPE_LIFETIME_RANK = {
|
|
3449
3637
|
application: 0,
|
|
@@ -3475,6 +3663,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3475
3663
|
"invalid-http-method-body": { code: "SC3004", docsUrl: "https://supacloud.dev/errors/SC3004" },
|
|
3476
3664
|
"unmatched-route-parameter": { code: "SC3005", docsUrl: "https://supacloud.dev/errors/SC3005" },
|
|
3477
3665
|
"missing-route-parameter-binding": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3666
|
+
"missing-path-param": { code: "SC3006", docsUrl: "https://supacloud.dev/errors/SC3006" },
|
|
3478
3667
|
"duplicate-route": { code: "SC3007", docsUrl: "https://supacloud.dev/errors/SC3007" },
|
|
3479
3668
|
"missing-body-schema": { code: "SC3008", docsUrl: "https://supacloud.dev/errors/SC3008" },
|
|
3480
3669
|
"unused-route-schema": { code: "SC3009", docsUrl: "https://supacloud.dev/errors/SC3009" },
|
|
@@ -3485,6 +3674,7 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3485
3674
|
"unmatched-path-param-decorator": { code: "SC3014", docsUrl: "https://supacloud.dev/errors/SC3014" },
|
|
3486
3675
|
"invalid-query-default-type": { code: "SC3015", docsUrl: "https://supacloud.dev/errors/SC3015" },
|
|
3487
3676
|
"disallowed-body-on-get-delete": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3677
|
+
"invalid-body-binding": { code: "SC3016", docsUrl: "https://supacloud.dev/errors/SC3016" },
|
|
3488
3678
|
"duplicate-query-param-binding": { code: "SC3017", docsUrl: "https://supacloud.dev/errors/SC3017" },
|
|
3489
3679
|
"conflicting-route-method": { code: "SC3018", docsUrl: "https://supacloud.dev/errors/SC3018" },
|
|
3490
3680
|
"missing-param-colon": { code: "SC3019", docsUrl: "https://supacloud.dev/errors/SC3019" },
|
|
@@ -3500,7 +3690,15 @@ var COMPILER_DIAGNOSTIC_CODES = {
|
|
|
3500
3690
|
"invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
|
|
3501
3691
|
"dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
|
|
3502
3692
|
"invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
|
|
3503
|
-
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" }
|
|
3693
|
+
"unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" },
|
|
3694
|
+
"invalid-feature-states": { code: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
|
|
3695
|
+
"duplicate-feature-transition": { code: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
|
|
3696
|
+
"invalid-feature-transition": { code: "SC6003", docsUrl: "https://supacloud.dev/errors/SC6003" },
|
|
3697
|
+
"feature-command-unresolved": { code: "SC6004", docsUrl: "https://supacloud.dev/errors/SC6004" },
|
|
3698
|
+
"feature-governance-drift": { code: "SC6005", docsUrl: "https://supacloud.dev/errors/SC6005" },
|
|
3699
|
+
"feature-route-unresolved": { code: "SC6006", docsUrl: "https://supacloud.dev/errors/SC6006" },
|
|
3700
|
+
"feature-route-drift": { code: "SC6007", docsUrl: "https://supacloud.dev/errors/SC6007" },
|
|
3701
|
+
"invalid-feature-spec": { code: "SC6008", docsUrl: "https://supacloud.dev/errors/SC6008" }
|
|
3504
3702
|
};
|
|
3505
3703
|
function validateGraph(graph, options = false) {
|
|
3506
3704
|
const strict = typeof options === "boolean" ? options : options.strict ?? false;
|
|
@@ -3556,7 +3754,7 @@ function validateGraph(graph, options = false) {
|
|
|
3556
3754
|
}
|
|
3557
3755
|
return;
|
|
3558
3756
|
}
|
|
3559
|
-
const error = (code, message, file, line, suggestion) => {
|
|
3757
|
+
const error = (code, message, file, line, suggestion, fix) => {
|
|
3560
3758
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3561
3759
|
diagnostics.push({
|
|
3562
3760
|
severity: "error",
|
|
@@ -3566,10 +3764,11 @@ function validateGraph(graph, options = false) {
|
|
|
3566
3764
|
line,
|
|
3567
3765
|
suggestion,
|
|
3568
3766
|
errorCode: meta?.code,
|
|
3569
|
-
docsUrl: meta?.docsUrl
|
|
3767
|
+
docsUrl: meta?.docsUrl,
|
|
3768
|
+
fix
|
|
3570
3769
|
});
|
|
3571
3770
|
};
|
|
3572
|
-
const warn2 = (code, message, file, line, suggestion) => {
|
|
3771
|
+
const warn2 = (code, message, file, line, suggestion, fix) => {
|
|
3573
3772
|
const meta = COMPILER_DIAGNOSTIC_CODES[code];
|
|
3574
3773
|
diagnostics.push({
|
|
3575
3774
|
severity: strict ? "error" : "warn",
|
|
@@ -3579,7 +3778,8 @@ function validateGraph(graph, options = false) {
|
|
|
3579
3778
|
line,
|
|
3580
3779
|
suggestion,
|
|
3581
3780
|
errorCode: meta?.code,
|
|
3582
|
-
docsUrl: meta?.docsUrl
|
|
3781
|
+
docsUrl: meta?.docsUrl,
|
|
3782
|
+
fix
|
|
3583
3783
|
});
|
|
3584
3784
|
};
|
|
3585
3785
|
const modulesByName = new Map;
|
|
@@ -3587,6 +3787,12 @@ function validateGraph(graph, options = false) {
|
|
|
3587
3787
|
const routesByKey = new Map;
|
|
3588
3788
|
const declaredRoutes = [];
|
|
3589
3789
|
for (const module of graph.modules) {
|
|
3790
|
+
if (module.featureSpec) {
|
|
3791
|
+
for (const diagnostic of validateFeatureSpec(module.featureSpec, module)) {
|
|
3792
|
+
const meta = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
|
|
3793
|
+
diagnostics.push({ ...diagnostic, errorCode: meta?.code, docsUrl: meta?.docsUrl });
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3590
3796
|
const previousModule = modulesByName.get(module.name);
|
|
3591
3797
|
if (previousModule) {
|
|
3592
3798
|
error("duplicate-module", `模块名 ${module.name} 重复(首次声明于 ${previousModule.file}:${previousModule.line})`, module.file, module.line);
|
|
@@ -3679,7 +3885,14 @@ function validateGraph(graph, options = false) {
|
|
|
3679
3885
|
if (paramBindings.length > 0) {
|
|
3680
3886
|
for (const param of pathParams) {
|
|
3681
3887
|
if (!paramBindings.includes(param)) {
|
|
3682
|
-
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
|
|
3888
|
+
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.`, {
|
|
3889
|
+
type: "add_route_parameter_binding",
|
|
3890
|
+
targetFile: controller.file,
|
|
3891
|
+
controller: controller.className,
|
|
3892
|
+
route: route.handler,
|
|
3893
|
+
parameter: param,
|
|
3894
|
+
binding: "param"
|
|
3895
|
+
});
|
|
3683
3896
|
}
|
|
3684
3897
|
}
|
|
3685
3898
|
}
|
|
@@ -3706,10 +3919,20 @@ function validateGraph(graph, options = false) {
|
|
|
3706
3919
|
}
|
|
3707
3920
|
}
|
|
3708
3921
|
if ((route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS" || route.method === "DELETE") && (route.hasBodyBinding || route.body)) {
|
|
3709
|
-
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()
|
|
3922
|
+
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().`, {
|
|
3923
|
+
type: "remove_route_body_binding",
|
|
3924
|
+
targetFile: controller.file,
|
|
3925
|
+
controller: controller.className,
|
|
3926
|
+
route: route.handler
|
|
3927
|
+
});
|
|
3710
3928
|
}
|
|
3711
3929
|
if (route.hasBodyBinding && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
3712
|
-
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()
|
|
3930
|
+
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().`, {
|
|
3931
|
+
type: "remove_route_body_binding",
|
|
3932
|
+
targetFile: controller.file,
|
|
3933
|
+
controller: controller.className,
|
|
3934
|
+
route: route.handler
|
|
3935
|
+
});
|
|
3713
3936
|
} else if (route.hasBodyBinding && !route.body) {
|
|
3714
3937
|
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.`);
|
|
3715
3938
|
} else if (route.body && !route.hasBodyBinding && !route.command) {
|
|
@@ -3850,23 +4073,51 @@ function validateGraph(graph, options = false) {
|
|
|
3850
4073
|
const owner = globalProviders.get(dep);
|
|
3851
4074
|
if (!owner)
|
|
3852
4075
|
continue;
|
|
3853
|
-
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' })
|
|
4076
|
+
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' }).`, {
|
|
4077
|
+
type: "add_module_import",
|
|
4078
|
+
targetFile: module.file,
|
|
4079
|
+
module: owner.module.name,
|
|
4080
|
+
provider: dep,
|
|
4081
|
+
symbol: owner.module.className,
|
|
4082
|
+
targetModule: module.name,
|
|
4083
|
+
importPath: (() => {
|
|
4084
|
+
const value = relative2(dirname2(module.file), owner.module.file).replace(/\.(tsx?|mts|cts)$/, "").split(sep2).join("/");
|
|
4085
|
+
return value.startsWith(".") ? value : `./${value}`;
|
|
4086
|
+
})()
|
|
4087
|
+
});
|
|
3854
4088
|
} else if (dep.includes("TOKEN") || dep.endsWith("Token") || dep.length > 2 && dep === dep.toUpperCase()) {
|
|
3855
4089
|
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: () => ... }).`);
|
|
3856
4090
|
} else {
|
|
3857
|
-
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' })
|
|
4091
|
+
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' }).`, {
|
|
4092
|
+
type: "add_provider",
|
|
4093
|
+
targetFile: module.file,
|
|
4094
|
+
token: dep,
|
|
4095
|
+
module: module.name
|
|
4096
|
+
});
|
|
3858
4097
|
}
|
|
3859
4098
|
}
|
|
3860
4099
|
continue;
|
|
3861
4100
|
}
|
|
3862
4101
|
if (SCOPE_LIFETIME_RANK[resolved.provider.scope] > SCOPE_LIFETIME_RANK[provider.scope]) {
|
|
3863
|
-
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
|
|
4102
|
+
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.`, {
|
|
4103
|
+
type: "change_provider_scope",
|
|
4104
|
+
targetFile: provider.file,
|
|
4105
|
+
provider: provider.token,
|
|
4106
|
+
from: provider.scope,
|
|
4107
|
+
to: resolved.provider.scope
|
|
4108
|
+
});
|
|
3864
4109
|
}
|
|
3865
4110
|
}
|
|
3866
4111
|
}
|
|
3867
4112
|
for (const command of module.commands) {
|
|
3868
4113
|
if (!command.permission) {
|
|
3869
|
-
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."
|
|
4114
|
+
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.", {
|
|
4115
|
+
type: "add_command_permission",
|
|
4116
|
+
targetFile: module.providers.find((provider) => provider.useClass === command.className || provider.token === command.className)?.file ?? module.file,
|
|
4117
|
+
command: command.className,
|
|
4118
|
+
module: module.name,
|
|
4119
|
+
permission: `${module.name}.${command.name}`
|
|
4120
|
+
});
|
|
3870
4121
|
}
|
|
3871
4122
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
3872
4123
|
const caps = options.commandCapabilities;
|
|
@@ -4220,7 +4471,7 @@ import { join as join4 } from "node:path";
|
|
|
4220
4471
|
|
|
4221
4472
|
// src/type-safety.ts
|
|
4222
4473
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
4223
|
-
import { dirname as
|
|
4474
|
+
import { dirname as dirname3, join as join3, relative as relative3, resolve as resolve2, sep as sep3 } from "node:path";
|
|
4224
4475
|
import * as ts4 from "@typescript/typescript6";
|
|
4225
4476
|
var DEFAULT_EXCLUDES = [
|
|
4226
4477
|
"**/*.test.ts",
|
|
@@ -4342,7 +4593,7 @@ function readProjectConfig2(configPath) {
|
|
|
4342
4593
|
const config = ts4.readConfigFile(configPath, (file) => readFileSync2(file, "utf8"));
|
|
4343
4594
|
if (config.error)
|
|
4344
4595
|
return { options: {}, errors: [config.error] };
|
|
4345
|
-
const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys,
|
|
4596
|
+
const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, dirname3(configPath));
|
|
4346
4597
|
return { options: parsed.options, errors: parsed.errors };
|
|
4347
4598
|
}
|
|
4348
4599
|
function isProductionSource(rootDir, sourceFile, excludes, outDir) {
|
|
@@ -4425,7 +4676,7 @@ function makeDiagnostic(code, message, fileOrSourceFile, node, strict, rootDir)
|
|
|
4425
4676
|
};
|
|
4426
4677
|
}
|
|
4427
4678
|
function normalizeRelative(rootDir, filePath) {
|
|
4428
|
-
return
|
|
4679
|
+
return relative3(rootDir, filePath).split(sep3).join("/").replace(/^\.\//, "");
|
|
4429
4680
|
}
|
|
4430
4681
|
function isAnyKeyword(node) {
|
|
4431
4682
|
return node.kind === ts4.SyntaxKind.AnyKeyword;
|
|
@@ -4615,6 +4866,61 @@ function explainGraph(graph, subject) {
|
|
|
4615
4866
|
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
4616
4867
|
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
4617
4868
|
}
|
|
4869
|
+
function createContextPack(graph, subject) {
|
|
4870
|
+
const subjectModule = graph.modules.find((module) => module.name === subject);
|
|
4871
|
+
if (!subjectModule) {
|
|
4872
|
+
throw new Error(`No module named "${subject}". Context packs require a module name.`);
|
|
4873
|
+
}
|
|
4874
|
+
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
4875
|
+
const selected = new Set([subjectModule.name]);
|
|
4876
|
+
const queue = [subjectModule.name];
|
|
4877
|
+
while (queue.length > 0) {
|
|
4878
|
+
const current = queue.shift();
|
|
4879
|
+
if (!current)
|
|
4880
|
+
continue;
|
|
4881
|
+
const module = byName.get(current);
|
|
4882
|
+
if (!module)
|
|
4883
|
+
continue;
|
|
4884
|
+
const neighbors = [
|
|
4885
|
+
...module.imports,
|
|
4886
|
+
...graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name)
|
|
4887
|
+
];
|
|
4888
|
+
for (const neighbor of neighbors) {
|
|
4889
|
+
if (!selected.has(neighbor) && byName.has(neighbor)) {
|
|
4890
|
+
selected.add(neighbor);
|
|
4891
|
+
queue.push(neighbor);
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
const modules = graph.modules.filter((module) => selected.has(module.name));
|
|
4896
|
+
const files = [...new Set(modules.flatMap((module) => [
|
|
4897
|
+
module.file,
|
|
4898
|
+
...module.providers.map((provider) => provider.file),
|
|
4899
|
+
...module.controllers.map((controller) => controller.file)
|
|
4900
|
+
]))].sort();
|
|
4901
|
+
const referencedTokens = new Set;
|
|
4902
|
+
for (const module of modules) {
|
|
4903
|
+
for (const provider of module.providers) {
|
|
4904
|
+
for (const token of provider.deps)
|
|
4905
|
+
referencedTokens.add(token);
|
|
4906
|
+
}
|
|
4907
|
+
for (const controller of module.controllers) {
|
|
4908
|
+
for (const token of controller.deps)
|
|
4909
|
+
referencedTokens.add(token);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
return {
|
|
4913
|
+
version: 1,
|
|
4914
|
+
subject: subjectModule.name,
|
|
4915
|
+
modules,
|
|
4916
|
+
files,
|
|
4917
|
+
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
4918
|
+
relatedModules: {
|
|
4919
|
+
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
4920
|
+
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
4921
|
+
}
|
|
4922
|
+
};
|
|
4923
|
+
}
|
|
4618
4924
|
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
4619
4925
|
const checks = [
|
|
4620
4926
|
{
|
|
@@ -4715,12 +5021,12 @@ function exportGraphDot(graph) {
|
|
|
4715
5021
|
|
|
4716
5022
|
// src/watch.ts
|
|
4717
5023
|
import { watch } from "node:fs";
|
|
4718
|
-
import { relative as
|
|
5024
|
+
import { relative as relative5, resolve as resolve4 } from "node:path";
|
|
4719
5025
|
|
|
4720
5026
|
// src/incremental.ts
|
|
4721
5027
|
import { createHash as createHash5 } from "node:crypto";
|
|
4722
5028
|
import { access as access2, readdir, readFile } from "node:fs/promises";
|
|
4723
|
-
import { isAbsolute, relative as
|
|
5029
|
+
import { isAbsolute, relative as relative4, resolve as resolve3, sep as sep4 } from "node:path";
|
|
4724
5030
|
function createDependencyGraphCache() {
|
|
4725
5031
|
return {
|
|
4726
5032
|
modules: new Map,
|
|
@@ -4799,12 +5105,12 @@ async function updateSnapshot(previous, options, changedPaths) {
|
|
|
4799
5105
|
const files = { ...previous.files };
|
|
4800
5106
|
for (const changedPath of changedPaths) {
|
|
4801
5107
|
const absolutePath = isAbsolute(changedPath) ? resolve3(changedPath) : resolve3(rootDir, changedPath);
|
|
4802
|
-
const relativeChangedPath =
|
|
4803
|
-
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${
|
|
5108
|
+
const relativeChangedPath = relative4(rootDir, absolutePath);
|
|
5109
|
+
if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep4}`))
|
|
4804
5110
|
continue;
|
|
4805
5111
|
if (absolutePath === outDir || absolutePath.startsWith(`${outDir}/`))
|
|
4806
5112
|
continue;
|
|
4807
|
-
const relativePath =
|
|
5113
|
+
const relativePath = relative4(rootDir, absolutePath).split(sep4).join("/");
|
|
4808
5114
|
try {
|
|
4809
5115
|
await access2(absolutePath);
|
|
4810
5116
|
const content = await readFile(absolutePath);
|
|
@@ -4822,7 +5128,7 @@ async function createSnapshot(options) {
|
|
|
4822
5128
|
const files = {};
|
|
4823
5129
|
for (const path of paths) {
|
|
4824
5130
|
const content = await readFile(path);
|
|
4825
|
-
files[
|
|
5131
|
+
files[relative4(rootDir, path).split(sep4).join("/")] = createHash5("sha256").update(content).digest("hex");
|
|
4826
5132
|
}
|
|
4827
5133
|
return { files, optionsKey: optionsKeyOf(options) };
|
|
4828
5134
|
}
|
|
@@ -5059,11 +5365,11 @@ function watchProject(options) {
|
|
|
5059
5365
|
if (!filename)
|
|
5060
5366
|
return schedule();
|
|
5061
5367
|
const changedPath = resolve4(rootDir, filename.toString());
|
|
5062
|
-
const relativePath =
|
|
5368
|
+
const relativePath = relative5(outDir, changedPath);
|
|
5063
5369
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
5064
5370
|
return;
|
|
5065
5371
|
if (/\.(tsx?|mts|cts)$/.test(changedPath))
|
|
5066
|
-
schedule(
|
|
5372
|
+
schedule(relative5(rootDir, changedPath));
|
|
5067
5373
|
});
|
|
5068
5374
|
if (initialEvent)
|
|
5069
5375
|
resolveReady(initialEvent);
|
|
@@ -5147,6 +5453,196 @@ function compileOptionsFromConfig(config, cwd = process.cwd()) {
|
|
|
5147
5453
|
};
|
|
5148
5454
|
}
|
|
5149
5455
|
|
|
5456
|
+
// src/fixes.ts
|
|
5457
|
+
import { randomUUID } from "node:crypto";
|
|
5458
|
+
import { lstat, readFile as readFile2, realpath, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
5459
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, relative as relative6, resolve as resolve6, sep as sep5 } from "node:path";
|
|
5460
|
+
import * as ts5 from "@typescript/typescript6";
|
|
5461
|
+
async function applyDiagnosticFix(fix, options = {}) {
|
|
5462
|
+
if (!fix || typeof fix.targetFile !== "string")
|
|
5463
|
+
throw new Error("Invalid DiagnosticFix");
|
|
5464
|
+
const root = await realpath(options.rootDir ?? process.cwd());
|
|
5465
|
+
const file = resolve6(root, fix.targetFile);
|
|
5466
|
+
const stat = await lstat(file);
|
|
5467
|
+
const resolved = await realpath(file);
|
|
5468
|
+
const relativePath = relative6(root, resolved);
|
|
5469
|
+
if (stat.isSymbolicLink() || !stat.isFile() || isAbsolute2(relativePath) || relativePath === ".." || relativePath.startsWith(`..${sep5}`)) {
|
|
5470
|
+
throw new Error("Fix target must be a regular file inside rootDir");
|
|
5471
|
+
}
|
|
5472
|
+
const original = await readFile2(file, "utf8");
|
|
5473
|
+
let source = parse(file, original);
|
|
5474
|
+
let content;
|
|
5475
|
+
switch (fix.type) {
|
|
5476
|
+
case "add_module_import": {
|
|
5477
|
+
if (!fix.importPath || !fix.symbol)
|
|
5478
|
+
throw new Error("Module fix requires importPath and symbol");
|
|
5479
|
+
identifier(fix.symbol);
|
|
5480
|
+
const withImport = importSymbol(source, fix.importPath, fix.symbol);
|
|
5481
|
+
source = parse(file, withImport);
|
|
5482
|
+
const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
|
|
5483
|
+
const imports = property(object, "imports");
|
|
5484
|
+
if (imports && !ts5.isArrayLiteralExpression(imports.initializer)) {
|
|
5485
|
+
throw new Error("Module imports must be a static array");
|
|
5486
|
+
}
|
|
5487
|
+
const values = imports && ts5.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
|
|
5488
|
+
if (values.some(ts5.isSpreadElement))
|
|
5489
|
+
throw new Error("Module imports cannot contain spread elements");
|
|
5490
|
+
content = values.some((value) => ts5.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts5.factory.createArrayLiteralExpression([
|
|
5491
|
+
...values,
|
|
5492
|
+
ts5.factory.createIdentifier(fix.symbol)
|
|
5493
|
+
]));
|
|
5494
|
+
break;
|
|
5495
|
+
}
|
|
5496
|
+
case "add_command_permission": {
|
|
5497
|
+
const permission = options.permission ?? fix.permission;
|
|
5498
|
+
if (!permission?.trim()) {
|
|
5499
|
+
throw new Error("Permission fix requires an explicit permission; privileges are never inferred");
|
|
5500
|
+
}
|
|
5501
|
+
const command = findClass(source, fix.command);
|
|
5502
|
+
const object = decoratorObject(command, "Command");
|
|
5503
|
+
const current = property(object, "permission");
|
|
5504
|
+
if (current && (!ts5.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
|
|
5505
|
+
throw new Error("Command permission already exists with a different value");
|
|
5506
|
+
}
|
|
5507
|
+
content = current ? original : replaceProperty(source, object, "permission", ts5.factory.createStringLiteral(permission));
|
|
5508
|
+
break;
|
|
5509
|
+
}
|
|
5510
|
+
case "add_route_parameter_binding": {
|
|
5511
|
+
const controller = findClass(source, fix.controller);
|
|
5512
|
+
const method = unique(controller.members.filter((member) => ts5.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
|
|
5513
|
+
const parameter = unique(method.parameters.filter((candidate) => ts5.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
|
|
5514
|
+
const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
|
|
5515
|
+
if (!binding)
|
|
5516
|
+
throw new Error("Invalid route binding");
|
|
5517
|
+
const decorators = ts5.getDecorators(parameter) ?? [];
|
|
5518
|
+
if (decorators.length > 0)
|
|
5519
|
+
throw new Error("Parameter already has a decorator");
|
|
5520
|
+
const framework = unique(source.statements.filter((statement) => ts5.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts5.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some((element) => ["Controller", "Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].includes(element.name.text))), "framework import");
|
|
5521
|
+
if (!ts5.isImportDeclaration(framework) || !ts5.isStringLiteral(framework.moduleSpecifier)) {
|
|
5522
|
+
throw new Error("Framework import must be static");
|
|
5523
|
+
}
|
|
5524
|
+
const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
|
|
5525
|
+
content = importSymbol(parse(file, edited), framework.moduleSpecifier.text, binding);
|
|
5526
|
+
break;
|
|
5527
|
+
}
|
|
5528
|
+
default:
|
|
5529
|
+
throw new Error(`Diagnostic fix '${fix.type}' requires a manual semantic decision; no files changed`);
|
|
5530
|
+
}
|
|
5531
|
+
parse(file, content);
|
|
5532
|
+
const result = { file, changed: content !== original, content };
|
|
5533
|
+
if (options.dryRun === false && result.changed) {
|
|
5534
|
+
const temporary = `${file}.supacloud-fix-${randomUUID()}`;
|
|
5535
|
+
try {
|
|
5536
|
+
await writeFile2(temporary, content, { encoding: "utf8", flag: "wx", mode: stat.mode });
|
|
5537
|
+
if (await readFile2(file, "utf8") !== original)
|
|
5538
|
+
throw new Error("Target changed while preparing the fix");
|
|
5539
|
+
await rename2(temporary, file);
|
|
5540
|
+
} finally {
|
|
5541
|
+
await unlink2(temporary).catch(() => {});
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
return result;
|
|
5545
|
+
}
|
|
5546
|
+
function parse(file, text) {
|
|
5547
|
+
const result = ts5.transpileModule(text, {
|
|
5548
|
+
fileName: file,
|
|
5549
|
+
reportDiagnostics: true,
|
|
5550
|
+
compilerOptions: { target: ts5.ScriptTarget.ESNext, experimentalDecorators: true }
|
|
5551
|
+
});
|
|
5552
|
+
if (result.diagnostics?.some((item) => item.category === ts5.DiagnosticCategory.Error)) {
|
|
5553
|
+
throw new Error("Cannot fix syntactically invalid TypeScript");
|
|
5554
|
+
}
|
|
5555
|
+
return ts5.createSourceFile(file, text, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
5556
|
+
}
|
|
5557
|
+
function unique(items, description) {
|
|
5558
|
+
if (items.length !== 1)
|
|
5559
|
+
throw new Error(`Expected exactly one ${description}; found ${items.length}`);
|
|
5560
|
+
return items[0];
|
|
5561
|
+
}
|
|
5562
|
+
function identifier(value) {
|
|
5563
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(value))
|
|
5564
|
+
throw new Error(`Invalid identifier '${value}'`);
|
|
5565
|
+
}
|
|
5566
|
+
function nameOf(name) {
|
|
5567
|
+
if (!name)
|
|
5568
|
+
return "";
|
|
5569
|
+
return ts5.isIdentifier(name) || ts5.isStringLiteral(name) || ts5.isNumericLiteral(name) ? name.text : "";
|
|
5570
|
+
}
|
|
5571
|
+
function property(object, key) {
|
|
5572
|
+
if (object.properties.some((item) => !ts5.isPropertyAssignment(item) || ts5.isComputedPropertyName(item.name))) {
|
|
5573
|
+
throw new Error("Fix requires explicit static object properties");
|
|
5574
|
+
}
|
|
5575
|
+
const values = object.properties.filter((item) => ts5.isPropertyAssignment(item) && nameOf(item.name) === key);
|
|
5576
|
+
if (values.length > 1)
|
|
5577
|
+
throw new Error(`Duplicate '${key}' property`);
|
|
5578
|
+
return values[0];
|
|
5579
|
+
}
|
|
5580
|
+
function stringProperty(object, key) {
|
|
5581
|
+
const value = property(object, key)?.initializer;
|
|
5582
|
+
return value && ts5.isStringLiteral(value) ? value.text : undefined;
|
|
5583
|
+
}
|
|
5584
|
+
function replaceProperty(source, object, key, value) {
|
|
5585
|
+
const previous = property(object, key);
|
|
5586
|
+
const replacement = ts5.factory.createPropertyAssignment(key, value);
|
|
5587
|
+
const properties = object.properties.map((item) => item === previous ? replacement : item);
|
|
5588
|
+
if (!previous)
|
|
5589
|
+
properties.push(replacement);
|
|
5590
|
+
const updated = ts5.factory.updateObjectLiteralExpression(object, properties);
|
|
5591
|
+
return source.text.slice(0, object.getStart(source)) + ts5.createPrinter().printNode(ts5.EmitHint.Expression, updated, source) + source.text.slice(object.end);
|
|
5592
|
+
}
|
|
5593
|
+
function findClass(source, name) {
|
|
5594
|
+
return unique(source.statements.filter((statement) => ts5.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
|
|
5595
|
+
}
|
|
5596
|
+
function decoratorObject(node, name) {
|
|
5597
|
+
const decorator = unique((ts5.getDecorators(node) ?? []).filter((item) => ts5.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
|
|
5598
|
+
const argument = ts5.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
|
|
5599
|
+
if (!argument || !ts5.isObjectLiteralExpression(argument))
|
|
5600
|
+
throw new Error(`@${name} requires a static object`);
|
|
5601
|
+
return argument;
|
|
5602
|
+
}
|
|
5603
|
+
function moduleObjects(source) {
|
|
5604
|
+
const result = [];
|
|
5605
|
+
for (const statement of source.statements) {
|
|
5606
|
+
if (ts5.isClassDeclaration(statement) && (ts5.getDecorators(statement) ?? []).some((item) => ts5.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
|
|
5607
|
+
result.push(decoratorObject(statement, "Module"));
|
|
5608
|
+
}
|
|
5609
|
+
if (ts5.isVariableStatement(statement)) {
|
|
5610
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
5611
|
+
const call = declaration.initializer;
|
|
5612
|
+
if (call && ts5.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts5.isObjectLiteralExpression(call.arguments[0])) {
|
|
5613
|
+
result.push(call.arguments[0]);
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
return result;
|
|
5619
|
+
}
|
|
5620
|
+
function importSymbol(source, path, symbol) {
|
|
5621
|
+
identifier(symbol);
|
|
5622
|
+
const current = resolve6(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
|
|
5623
|
+
const target = resolve6(dirname4(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
|
|
5624
|
+
if (current === target)
|
|
5625
|
+
return source.text;
|
|
5626
|
+
const matches = source.statements.filter((item) => ts5.isImportDeclaration(item) && ts5.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
|
|
5627
|
+
if (matches.length > 1)
|
|
5628
|
+
throw new Error(`Ambiguous imports from '${path}'`);
|
|
5629
|
+
const match = matches[0];
|
|
5630
|
+
if (match && ts5.isImportDeclaration(match) && match.importClause?.namedBindings && ts5.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
|
|
5631
|
+
if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
|
|
5632
|
+
return source.text;
|
|
5633
|
+
const bindings = match.importClause.namedBindings;
|
|
5634
|
+
const updated = ts5.factory.updateNamedImports(bindings, [
|
|
5635
|
+
...bindings.elements,
|
|
5636
|
+
ts5.factory.createImportSpecifier(false, undefined, ts5.factory.createIdentifier(symbol))
|
|
5637
|
+
]);
|
|
5638
|
+
return source.text.slice(0, bindings.getStart(source)) + ts5.createPrinter().printNode(ts5.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
|
|
5639
|
+
}
|
|
5640
|
+
if (match)
|
|
5641
|
+
throw new Error(`Import from '${path}' is not a named value import`);
|
|
5642
|
+
return `import { ${symbol} } from ${JSON.stringify(path)};
|
|
5643
|
+
${source.text}`;
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5150
5646
|
// src/cli.ts
|
|
5151
5647
|
function isModuleBoundaryPresetName(value) {
|
|
5152
5648
|
return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
|
|
@@ -5161,7 +5657,9 @@ Usage:
|
|
|
5161
5657
|
supacloud-compiler dev [rootDir] [options]
|
|
5162
5658
|
supacloud-compiler graph [rootDir] [options]
|
|
5163
5659
|
supacloud-compiler explain <name> [rootDir] [options]
|
|
5660
|
+
supacloud-compiler context <module> [rootDir] [options]
|
|
5164
5661
|
supacloud-compiler doctor [rootDir] [options]
|
|
5662
|
+
supacloud-compiler fix <fix.json> [options]
|
|
5165
5663
|
|
|
5166
5664
|
Commands:
|
|
5167
5665
|
compile Compile application modules and generate artifacts
|
|
@@ -5169,6 +5667,7 @@ Commands:
|
|
|
5169
5667
|
dev Watch source files and recompile on changes
|
|
5170
5668
|
graph Print the discovered application graph
|
|
5171
5669
|
explain Explain a module, provider, or external token
|
|
5670
|
+
context Extract an AI-sized module context pack
|
|
5172
5671
|
doctor Run project and generated-artifact health checks
|
|
5173
5672
|
|
|
5174
5673
|
Options:
|
|
@@ -5181,7 +5680,9 @@ Options:
|
|
|
5181
5680
|
--permissions Generate typed permissions registry (default)
|
|
5182
5681
|
--no-permissions Do not generate permissions.ts
|
|
5183
5682
|
--debounce <ms> Debounce source changes in dev mode (default: 100)
|
|
5184
|
-
--json Print machine-readable output for graph/explain/doctor
|
|
5683
|
+
--json Print machine-readable output for compile/check/graph/explain/context/doctor
|
|
5684
|
+
--dry-run Preview a fix without writing the target file
|
|
5685
|
+
--write Apply a fix to disk (fix is preview-only by default)
|
|
5185
5686
|
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
5186
5687
|
--help, -h Show this help
|
|
5187
5688
|
`);
|
|
@@ -5193,7 +5694,7 @@ async function run() {
|
|
|
5193
5694
|
process.exit(0);
|
|
5194
5695
|
}
|
|
5195
5696
|
const command = args[0];
|
|
5196
|
-
if (!["compile", "check", "dev", "graph", "explain", "doctor"].includes(command)) {
|
|
5697
|
+
if (!["compile", "check", "dev", "graph", "explain", "context", "doctor", "fix"].includes(command)) {
|
|
5197
5698
|
console.error(`Error: unknown command "${command}"`);
|
|
5198
5699
|
printUsage();
|
|
5199
5700
|
process.exit(1);
|
|
@@ -5207,6 +5708,7 @@ async function run() {
|
|
|
5207
5708
|
let debounceMs = 100;
|
|
5208
5709
|
let query;
|
|
5209
5710
|
let json = false;
|
|
5711
|
+
let dryRun = true;
|
|
5210
5712
|
for (let i = 1;i < args.length; i++) {
|
|
5211
5713
|
const arg = args[i];
|
|
5212
5714
|
if (arg === "--root" || arg === "-r") {
|
|
@@ -5233,6 +5735,10 @@ async function run() {
|
|
|
5233
5735
|
}
|
|
5234
5736
|
} else if (arg === "--json") {
|
|
5235
5737
|
json = true;
|
|
5738
|
+
} else if (arg === "--dry-run") {
|
|
5739
|
+
dryRun = true;
|
|
5740
|
+
} else if (arg === "--write") {
|
|
5741
|
+
dryRun = false;
|
|
5236
5742
|
} else if (arg === "--preset" || arg === "-p") {
|
|
5237
5743
|
const presetArg = args[++i];
|
|
5238
5744
|
if (!isModuleBoundaryPresetName(presetArg)) {
|
|
@@ -5241,18 +5747,18 @@ async function run() {
|
|
|
5241
5747
|
}
|
|
5242
5748
|
preset = presetArg;
|
|
5243
5749
|
} else if (!arg.startsWith("-") && !rootDir) {
|
|
5244
|
-
if (command === "explain" && !query)
|
|
5750
|
+
if ((command === "explain" || command === "context" || command === "fix") && !query)
|
|
5245
5751
|
query = arg;
|
|
5246
5752
|
else
|
|
5247
5753
|
rootDir = arg;
|
|
5248
|
-
} else if (!arg.startsWith("-") && command === "explain" && !query) {
|
|
5754
|
+
} else if (!arg.startsWith("-") && (command === "explain" || command === "context" || command === "fix") && !query) {
|
|
5249
5755
|
query = arg;
|
|
5250
5756
|
}
|
|
5251
5757
|
}
|
|
5252
5758
|
const loadedConfig = await loadSupacloudConfig(process.cwd());
|
|
5253
5759
|
const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
|
|
5254
|
-
const resolvedRoot = rootDir ?
|
|
5255
|
-
const resolvedOut = outDir ?
|
|
5760
|
+
const resolvedRoot = rootDir ? resolve7(process.cwd(), rootDir) : defaults.rootDir;
|
|
5761
|
+
const resolvedOut = outDir ? resolve7(process.cwd(), outDir) : defaults.outDir;
|
|
5256
5762
|
const configured = compileOptionsFromConfig({
|
|
5257
5763
|
...loadedConfig,
|
|
5258
5764
|
root: resolvedRoot,
|
|
@@ -5265,38 +5771,69 @@ async function run() {
|
|
|
5265
5771
|
...configured,
|
|
5266
5772
|
moduleBoundaryPreset: preset ?? configured.moduleBoundaryPreset
|
|
5267
5773
|
};
|
|
5268
|
-
if (command === "
|
|
5774
|
+
if (command === "fix") {
|
|
5775
|
+
if (!query)
|
|
5776
|
+
throw new Error("fix requires a JSON file containing one DiagnosticFix");
|
|
5777
|
+
const fix = JSON.parse(await readFile3(resolve7(process.cwd(), query), "utf8"));
|
|
5778
|
+
const result = await applyDiagnosticFix(fix, { rootDir: process.cwd(), dryRun });
|
|
5779
|
+
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
5780
|
+
} else if (command === "compile") {
|
|
5269
5781
|
const result = await compileProject(compileDefaults);
|
|
5270
|
-
printDiagnostics(result.diagnostics);
|
|
5271
5782
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
5783
|
+
if (json) {
|
|
5784
|
+
console.log(JSON.stringify({
|
|
5785
|
+
ok: errors.length === 0,
|
|
5786
|
+
diagnostics: result.diagnostics,
|
|
5787
|
+
written: result.written,
|
|
5788
|
+
stats: result.stats
|
|
5789
|
+
}, null, 2));
|
|
5790
|
+
} else {
|
|
5791
|
+
printDiagnostics(result.diagnostics);
|
|
5792
|
+
}
|
|
5272
5793
|
if (errors.length > 0) {
|
|
5273
|
-
|
|
5794
|
+
if (!json)
|
|
5795
|
+
console.error(`
|
|
5274
5796
|
Compilation failed with ${errors.length} error(s).`);
|
|
5275
5797
|
process.exit(1);
|
|
5276
5798
|
}
|
|
5277
|
-
|
|
5799
|
+
if (!json) {
|
|
5800
|
+
console.log(`
|
|
5278
5801
|
Compilation succeeded. Generated artifacts:
|
|
5279
5802
|
${result.written.map((f) => ` - ${f}`).join(`
|
|
5280
5803
|
`)}`);
|
|
5804
|
+
}
|
|
5281
5805
|
} else if (command === "check") {
|
|
5282
5806
|
const result = await checkProject(compileDefaults);
|
|
5283
|
-
printDiagnostics(result.diagnostics);
|
|
5284
5807
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
5808
|
+
if (json) {
|
|
5809
|
+
console.log(JSON.stringify({
|
|
5810
|
+
ok: errors.length === 0 && result.upToDate,
|
|
5811
|
+
upToDate: result.upToDate,
|
|
5812
|
+
mismatches: result.mismatches,
|
|
5813
|
+
diagnostics: result.diagnostics
|
|
5814
|
+
}, null, 2));
|
|
5815
|
+
} else {
|
|
5816
|
+
printDiagnostics(result.diagnostics);
|
|
5817
|
+
}
|
|
5285
5818
|
if (errors.length > 0) {
|
|
5286
|
-
|
|
5819
|
+
if (!json)
|
|
5820
|
+
console.error(`
|
|
5287
5821
|
Governance checks failed with ${errors.length} error(s).`);
|
|
5288
5822
|
process.exit(1);
|
|
5289
5823
|
}
|
|
5290
5824
|
if (!result.upToDate) {
|
|
5291
|
-
|
|
5825
|
+
if (!json) {
|
|
5826
|
+
console.error(`
|
|
5292
5827
|
Artifact drift detected:`);
|
|
5293
|
-
|
|
5294
|
-
|
|
5828
|
+
for (const mismatch of result.mismatches) {
|
|
5829
|
+
console.error(` - ${mismatch}`);
|
|
5830
|
+
}
|
|
5831
|
+
console.error("Run the compile command and commit the updated generated artifacts.");
|
|
5295
5832
|
}
|
|
5296
|
-
console.error("Run the compile command and commit the updated generated artifacts.");
|
|
5297
5833
|
process.exit(1);
|
|
5298
5834
|
}
|
|
5299
|
-
|
|
5835
|
+
if (!json)
|
|
5836
|
+
console.log("Artifact check passed: disk files match compiler output with no drift.");
|
|
5300
5837
|
} else if (command === "dev") {
|
|
5301
5838
|
const handle = watchProject({
|
|
5302
5839
|
...compileDefaults,
|
|
@@ -5350,6 +5887,38 @@ Source change detected; compiling...`);
|
|
|
5350
5887
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
5351
5888
|
process.exit(1);
|
|
5352
5889
|
}
|
|
5890
|
+
} else if (command === "context") {
|
|
5891
|
+
if (!query) {
|
|
5892
|
+
console.error("Error: context requires a module name");
|
|
5893
|
+
process.exit(1);
|
|
5894
|
+
}
|
|
5895
|
+
try {
|
|
5896
|
+
const graph = await analyzeProject(resolvedRoot);
|
|
5897
|
+
const pack = createContextPack(graph, query);
|
|
5898
|
+
if (json) {
|
|
5899
|
+
console.log(JSON.stringify(pack, null, 2));
|
|
5900
|
+
} else {
|
|
5901
|
+
console.log([
|
|
5902
|
+
`CONTEXT ${pack.subject}`,
|
|
5903
|
+
` modules: ${pack.modules.map((module) => module.name).join(", ") || "-"}`,
|
|
5904
|
+
` files: ${pack.files.join(", ") || "-"}`,
|
|
5905
|
+
` external tokens: ${pack.externalTokens.join(", ") || "-"}`,
|
|
5906
|
+
` imports: ${pack.relatedModules.imports.join(", ") || "-"}`,
|
|
5907
|
+
` imported by: ${pack.relatedModules.importedBy.join(", ") || "-"}`
|
|
5908
|
+
].join(`
|
|
5909
|
+
`));
|
|
5910
|
+
}
|
|
5911
|
+
} catch (error) {
|
|
5912
|
+
if (json) {
|
|
5913
|
+
console.log(JSON.stringify({
|
|
5914
|
+
ok: false,
|
|
5915
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5916
|
+
}, null, 2));
|
|
5917
|
+
} else {
|
|
5918
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
5919
|
+
}
|
|
5920
|
+
process.exit(1);
|
|
5921
|
+
}
|
|
5353
5922
|
} else {
|
|
5354
5923
|
const result = await checkProject(compileDefaults);
|
|
5355
5924
|
const doctor = doctorProject(resolvedRoot, resolvedOut, result.graph, result.upToDate, result.diagnostics);
|