@supacloud/compiler 0.24.0 → 0.25.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 +18 -0
- package/dist/cli.js +290 -44
- package/dist/contract-manifest.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +945 -737
- package/dist/migration-assess.d.ts +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,24 @@ protocol migration are documented in `docs/fa-consumer-governance.md` in the
|
|
|
96
96
|
repository. `context <module> --json` reports `routeContracts` and standalone
|
|
97
97
|
command execution plans; these are declarations and obligations, not runtime proof.
|
|
98
98
|
|
|
99
|
+
## Migration Assessment
|
|
100
|
+
|
|
101
|
+
`migration-assess` creates a local, read-only compatibility report from the
|
|
102
|
+
installed tested tuple, generated contract manifest, compiler drift checks and
|
|
103
|
+
an optional OpenAPI baseline/current pair:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bunx supacloud-compiler migration-assess --json \
|
|
107
|
+
--baseline-openapi artifacts/openapi-baseline.json \
|
|
108
|
+
--current-openapi artifacts/openapi-current.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The report distinguishes compatible changes, review items, breaking changes and
|
|
112
|
+
evidence that is not yet proven. It records artifact hashes and never writes
|
|
113
|
+
files, databases or remote environments. Rendering is explicitly independent:
|
|
114
|
+
CSR/SPA, SvelteKit, Nuxt, SSR, edge and trusted-server arrangements are valid;
|
|
115
|
+
SSR is never a migration prerequisite.
|
|
116
|
+
|
|
99
117
|
SupaCloud 应用静态编译器:读取 `@supacloud/app` 装饰器元数据的原生 TypeScript AST,构建 ApplicationGraph,做静态校验,并生成**无反射、无容器**的工厂代码与 manifest。
|
|
100
118
|
|
|
101
119
|
本包不依赖 `@supacloud/app`:AST 只按装饰器名匹配(`Module`/`Injectable`/`Inject`/`Command`/`Query`/`Controller`/`Get`/`Post`/`Put`/`Patch`/`Delete`/`defineModule`/`InjectionToken`),不校验 import 来源。
|
package/dist/cli.js
CHANGED
|
@@ -126,6 +126,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
126
126
|
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
127
127
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
128
128
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
129
|
+
...route.responses === undefined ? {} : {
|
|
130
|
+
responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
|
|
131
|
+
},
|
|
129
132
|
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
|
|
130
133
|
});
|
|
131
134
|
}
|
|
@@ -2697,12 +2700,12 @@ var init_graphql = __esm(() => {
|
|
|
2697
2700
|
});
|
|
2698
2701
|
|
|
2699
2702
|
// src/database-contracts.ts
|
|
2700
|
-
import { createHash as
|
|
2701
|
-
import { mkdir as mkdir5, readFile as
|
|
2702
|
-
import { dirname as dirname10, relative as relative12, resolve as
|
|
2703
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
2704
|
+
import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
|
|
2705
|
+
import { dirname as dirname10, relative as relative12, resolve as resolve16 } from "node:path";
|
|
2703
2706
|
import * as ts13 from "@typescript/typescript6";
|
|
2704
2707
|
function hash2(value) {
|
|
2705
|
-
return
|
|
2708
|
+
return createHash10("sha256").update(value).digest("hex");
|
|
2706
2709
|
}
|
|
2707
2710
|
function importPath(out, path) {
|
|
2708
2711
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2727,19 +2730,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2727
2730
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2728
2731
|
}
|
|
2729
2732
|
return {
|
|
2730
|
-
rootDir:
|
|
2731
|
-
outDir:
|
|
2732
|
-
postgrestTypes:
|
|
2733
|
-
drizzleSchema:
|
|
2733
|
+
rootDir: resolve16(directory, field("rootDir")),
|
|
2734
|
+
outDir: resolve16(directory, field("outDir")),
|
|
2735
|
+
postgrestTypes: resolve16(directory, field("postgrestTypes")),
|
|
2736
|
+
drizzleSchema: resolve16(directory, field("drizzleSchema")),
|
|
2734
2737
|
role: field("role"),
|
|
2735
|
-
graphql: { ...graphql, schema:
|
|
2736
|
-
migrations: migrations.map((file) =>
|
|
2738
|
+
graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
|
|
2739
|
+
migrations: migrations.map((file) => resolve16(directory, file))
|
|
2737
2740
|
};
|
|
2738
2741
|
}
|
|
2739
2742
|
async function generateDatabaseContracts(options, check = false) {
|
|
2740
|
-
const rootDir =
|
|
2741
|
-
const postgrestTypes =
|
|
2742
|
-
const snapshot = await
|
|
2743
|
+
const rootDir = resolve16(options.rootDir), outDir = resolve16(options.outDir);
|
|
2744
|
+
const postgrestTypes = resolve16(rootDir, options.postgrestTypes), drizzleSchema = resolve16(rootDir, options.drizzleSchema);
|
|
2745
|
+
const snapshot = await readFile12(postgrestTypes, "utf8");
|
|
2743
2746
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2744
2747
|
const database = syntax.statements.find((node) => (ts13.isTypeAliasDeclaration(node) || ts13.isInterfaceDeclaration(node)) && node.name.text === "Database" && node.modifiers?.some((modifier) => modifier.kind === ts13.SyntaxKind.ExportKeyword));
|
|
2745
2748
|
if (!database)
|
|
@@ -2766,8 +2769,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2766
2769
|
}
|
|
2767
2770
|
const inputs = {};
|
|
2768
2771
|
const addInput = async (path) => {
|
|
2769
|
-
const absolute =
|
|
2770
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2772
|
+
const absolute = resolve16(rootDir, path);
|
|
2773
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile12(absolute, "utf8"));
|
|
2771
2774
|
};
|
|
2772
2775
|
await addInput(postgrestTypes);
|
|
2773
2776
|
await addInput(drizzleSchema);
|
|
@@ -2783,8 +2786,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2783
2786
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2784
2787
|
await addInput(source.fileName);
|
|
2785
2788
|
}
|
|
2786
|
-
await addInput(
|
|
2787
|
-
if (new Set(options.migrations.map((path) =>
|
|
2789
|
+
await addInput(resolve16(rootDir, options.graphql.schema));
|
|
2790
|
+
if (new Set(options.migrations.map((path) => resolve16(rootDir, path))).size !== options.migrations.length) {
|
|
2788
2791
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2789
2792
|
}
|
|
2790
2793
|
for (const path of options.migrations)
|
|
@@ -2805,17 +2808,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2805
2808
|
version: 1,
|
|
2806
2809
|
role: options.role,
|
|
2807
2810
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2808
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2811
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
|
|
2809
2812
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2810
2813
|
};
|
|
2811
2814
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2812
2815
|
`;
|
|
2813
2816
|
const mismatches = [];
|
|
2814
2817
|
for (const [file, content] of Object.entries(files)) {
|
|
2815
|
-
const path =
|
|
2818
|
+
const path = resolve16(outDir, file);
|
|
2816
2819
|
let current;
|
|
2817
2820
|
try {
|
|
2818
|
-
current = await
|
|
2821
|
+
current = await readFile12(path, "utf8");
|
|
2819
2822
|
} catch (error) {
|
|
2820
2823
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2821
2824
|
throw error;
|
|
@@ -2826,13 +2829,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2826
2829
|
if (!check) {
|
|
2827
2830
|
await mkdir5(outDir, { recursive: true });
|
|
2828
2831
|
for (const [file, content] of Object.entries(files))
|
|
2829
|
-
await writeFileIfChanged(
|
|
2832
|
+
await writeFileIfChanged(resolve16(outDir, file), content);
|
|
2830
2833
|
}
|
|
2831
2834
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2832
2835
|
}
|
|
2833
2836
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2834
|
-
const absolute =
|
|
2835
|
-
const value = JSON.parse(await
|
|
2837
|
+
const absolute = resolve16(path);
|
|
2838
|
+
const value = JSON.parse(await readFile12(absolute, "utf8"));
|
|
2836
2839
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname10(absolute)), check);
|
|
2837
2840
|
}
|
|
2838
2841
|
var init_database_contracts = __esm(() => {
|
|
@@ -2842,9 +2845,9 @@ var init_database_contracts = __esm(() => {
|
|
|
2842
2845
|
});
|
|
2843
2846
|
|
|
2844
2847
|
// src/graphql-schema.ts
|
|
2845
|
-
import { mkdir as mkdir6, readFile as
|
|
2846
|
-
import { createHash as
|
|
2847
|
-
import { dirname as dirname11, resolve as
|
|
2848
|
+
import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
|
|
2849
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
2850
|
+
import { dirname as dirname11, resolve as resolve17 } from "node:path";
|
|
2848
2851
|
async function pullGraphqlSchema(options) {
|
|
2849
2852
|
assertGraphqlOptions({ schema: options.output });
|
|
2850
2853
|
const endpoint = new URL(options.url);
|
|
@@ -2884,7 +2887,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2884
2887
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2885
2888
|
}
|
|
2886
2889
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2887
|
-
const path =
|
|
2890
|
+
const path = resolve17(options.output);
|
|
2888
2891
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2889
2892
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2890
2893
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2892,13 +2895,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2892
2895
|
`;
|
|
2893
2896
|
let previous;
|
|
2894
2897
|
try {
|
|
2895
|
-
previous = await
|
|
2898
|
+
previous = await readFile13(path, "utf8");
|
|
2896
2899
|
} catch (error) {
|
|
2897
2900
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2898
2901
|
throw error;
|
|
2899
2902
|
}
|
|
2900
2903
|
const upToDate = previous === content;
|
|
2901
|
-
const schemaHash =
|
|
2904
|
+
const schemaHash = createHash11("sha256").update(content).digest("hex");
|
|
2902
2905
|
if (options.check)
|
|
2903
2906
|
return { path, schemaHash, upToDate, written: false };
|
|
2904
2907
|
await mkdir6(dirname11(path), { recursive: true });
|
|
@@ -2911,8 +2914,8 @@ var init_graphql_schema = __esm(() => {
|
|
|
2911
2914
|
});
|
|
2912
2915
|
|
|
2913
2916
|
// src/cli.ts
|
|
2914
|
-
import { resolve as
|
|
2915
|
-
import { readFile as
|
|
2917
|
+
import { resolve as resolve18 } from "node:path";
|
|
2918
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
2916
2919
|
|
|
2917
2920
|
// src/analyze.ts
|
|
2918
2921
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -7611,6 +7614,8 @@ function optionsKeyOf(options) {
|
|
|
7611
7614
|
requireRouteContracts: options.requireRouteContracts,
|
|
7612
7615
|
detectOrphanModules: options.detectOrphanModules,
|
|
7613
7616
|
generateClient: options.generateClient,
|
|
7617
|
+
generateOpenApi: options.generateOpenApi,
|
|
7618
|
+
openApi: options.openApi,
|
|
7614
7619
|
generatePermissions: options.generatePermissions,
|
|
7615
7620
|
typeSafety: options.typeSafety,
|
|
7616
7621
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -7745,6 +7750,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
7745
7750
|
|
|
7746
7751
|
// src/watch.ts
|
|
7747
7752
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
7753
|
+
function isCompilerConfigurationPath(rootDir, changedPath) {
|
|
7754
|
+
const relativePath = relative7(rootDir, changedPath).split(sep7).join("/");
|
|
7755
|
+
return [
|
|
7756
|
+
"supacloud.config.ts",
|
|
7757
|
+
"supacloud.config.mts",
|
|
7758
|
+
"supacloud.config.js",
|
|
7759
|
+
"supacloud.config.mjs",
|
|
7760
|
+
"tsconfig.json"
|
|
7761
|
+
].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
|
|
7762
|
+
}
|
|
7748
7763
|
function watchProject(options) {
|
|
7749
7764
|
const rootDir = resolve6(options.rootDir);
|
|
7750
7765
|
const outDir = resolve6(options.outDir);
|
|
@@ -7837,7 +7852,7 @@ function watchProject(options) {
|
|
|
7837
7852
|
const relativePath = relative7(outDir, changedPath);
|
|
7838
7853
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
7839
7854
|
return;
|
|
7840
|
-
if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
7855
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
7841
7856
|
schedule(relative7(rootDir, changedPath));
|
|
7842
7857
|
}
|
|
7843
7858
|
});
|
|
@@ -12716,6 +12731,11 @@ async function migrateProject(options) {
|
|
|
12716
12731
|
};
|
|
12717
12732
|
}
|
|
12718
12733
|
|
|
12734
|
+
// src/migration-assess.ts
|
|
12735
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
12736
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
12737
|
+
import { join as join9, resolve as resolve15 } from "node:path";
|
|
12738
|
+
|
|
12719
12739
|
// src/openapi-tools.ts
|
|
12720
12740
|
import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
12721
12741
|
import { dirname as dirname9, resolve as resolve14 } from "node:path";
|
|
@@ -13221,10 +13241,199 @@ function formatOpenApiDiff(result) {
|
|
|
13221
13241
|
`);
|
|
13222
13242
|
}
|
|
13223
13243
|
|
|
13244
|
+
// src/migration-assess.ts
|
|
13245
|
+
var STATUS_PRIORITY = {
|
|
13246
|
+
compatible: 0,
|
|
13247
|
+
"needs-review": 1,
|
|
13248
|
+
"not-proven": 2,
|
|
13249
|
+
unsupported: 3,
|
|
13250
|
+
breaking: 4
|
|
13251
|
+
};
|
|
13252
|
+
function isRecord2(value) {
|
|
13253
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13254
|
+
}
|
|
13255
|
+
function arrayLength(value) {
|
|
13256
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
13257
|
+
}
|
|
13258
|
+
async function sha256(path) {
|
|
13259
|
+
return createHash9("sha256").update(await readFile11(path)).digest("hex");
|
|
13260
|
+
}
|
|
13261
|
+
function overallStatus(findings) {
|
|
13262
|
+
return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
|
|
13263
|
+
}
|
|
13264
|
+
async function assessMigration(options) {
|
|
13265
|
+
const projectDir = resolve15(options.projectDir);
|
|
13266
|
+
const compile = {
|
|
13267
|
+
...options.compile,
|
|
13268
|
+
rootDir: resolve15(options.compile.rootDir),
|
|
13269
|
+
outDir: resolve15(options.compile.outDir)
|
|
13270
|
+
};
|
|
13271
|
+
const findings = [];
|
|
13272
|
+
const dependencyProblems = await checkMigrationDependencies(projectDir);
|
|
13273
|
+
if (dependencyProblems.length > 0) {
|
|
13274
|
+
findings.push({
|
|
13275
|
+
code: "migration-dependency-tuple-not-proven",
|
|
13276
|
+
status: "not-proven",
|
|
13277
|
+
message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
|
|
13278
|
+
evidence: dependencyProblems.join("; "),
|
|
13279
|
+
remediation: "Install the exact tested package versions before applying a source migration."
|
|
13280
|
+
});
|
|
13281
|
+
}
|
|
13282
|
+
let compiler = {
|
|
13283
|
+
upToDate: false,
|
|
13284
|
+
diagnostics: [],
|
|
13285
|
+
mismatches: []
|
|
13286
|
+
};
|
|
13287
|
+
try {
|
|
13288
|
+
const checked = await checkProject(compile);
|
|
13289
|
+
compiler = {
|
|
13290
|
+
upToDate: checked.upToDate,
|
|
13291
|
+
diagnostics: checked.diagnostics,
|
|
13292
|
+
mismatches: checked.mismatches
|
|
13293
|
+
};
|
|
13294
|
+
const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
13295
|
+
if (errors.length > 0) {
|
|
13296
|
+
findings.push({
|
|
13297
|
+
code: "compiler-governance-not-proven",
|
|
13298
|
+
status: "not-proven",
|
|
13299
|
+
message: `Compiler governance has ${errors.length} error(s).`,
|
|
13300
|
+
remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
|
|
13301
|
+
});
|
|
13302
|
+
}
|
|
13303
|
+
if (!checked.upToDate) {
|
|
13304
|
+
findings.push({
|
|
13305
|
+
code: "generated-artifact-drift",
|
|
13306
|
+
status: "needs-review",
|
|
13307
|
+
message: "Generated artifacts do not match the current source and compiler configuration.",
|
|
13308
|
+
evidence: checked.mismatches.join("; "),
|
|
13309
|
+
remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
|
|
13310
|
+
});
|
|
13311
|
+
}
|
|
13312
|
+
} catch {
|
|
13313
|
+
findings.push({
|
|
13314
|
+
code: "compiler-check-unavailable",
|
|
13315
|
+
status: "not-proven",
|
|
13316
|
+
message: "The local compiler check could not complete.",
|
|
13317
|
+
remediation: "Verify the project source, configuration and installed dependencies, then reassess."
|
|
13318
|
+
});
|
|
13319
|
+
}
|
|
13320
|
+
const contractsManifestPath = join9(compile.outDir, "contracts.manifest.json");
|
|
13321
|
+
let contractsManifest = {
|
|
13322
|
+
path: contractsManifestPath,
|
|
13323
|
+
present: false
|
|
13324
|
+
};
|
|
13325
|
+
try {
|
|
13326
|
+
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
13327
|
+
if (!isRecord2(parsed) || parsed.version !== 1)
|
|
13328
|
+
throw new Error("invalid manifest");
|
|
13329
|
+
contractsManifest = {
|
|
13330
|
+
path: contractsManifestPath,
|
|
13331
|
+
present: true,
|
|
13332
|
+
sha256: await sha256(contractsManifestPath),
|
|
13333
|
+
version: 1,
|
|
13334
|
+
...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
|
|
13335
|
+
...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
|
|
13336
|
+
...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
|
|
13337
|
+
};
|
|
13338
|
+
} catch {
|
|
13339
|
+
findings.push({
|
|
13340
|
+
code: "contract-manifest-not-proven",
|
|
13341
|
+
status: "not-proven",
|
|
13342
|
+
message: "contracts.manifest.json is missing or invalid.",
|
|
13343
|
+
evidence: contractsManifestPath,
|
|
13344
|
+
remediation: "Generate and review the contract manifest before migration."
|
|
13345
|
+
});
|
|
13346
|
+
}
|
|
13347
|
+
let openApi;
|
|
13348
|
+
if (options.baselineOpenApiPath && options.currentOpenApiPath) {
|
|
13349
|
+
const baselinePath = resolve15(projectDir, options.baselineOpenApiPath);
|
|
13350
|
+
const currentPath = resolve15(projectDir, options.currentOpenApiPath);
|
|
13351
|
+
try {
|
|
13352
|
+
const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
|
|
13353
|
+
openApi = {
|
|
13354
|
+
baselinePath,
|
|
13355
|
+
baselineSha256: await sha256(baselinePath),
|
|
13356
|
+
currentPath,
|
|
13357
|
+
currentSha256: await sha256(currentPath),
|
|
13358
|
+
diff
|
|
13359
|
+
};
|
|
13360
|
+
if (!diff.ok) {
|
|
13361
|
+
findings.push({
|
|
13362
|
+
code: "openapi-breaking-change",
|
|
13363
|
+
status: "breaking",
|
|
13364
|
+
message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
|
|
13365
|
+
remediation: "Review and explicitly approve or redesign each breaking contract change."
|
|
13366
|
+
});
|
|
13367
|
+
} else if (diff.changes.length > 0) {
|
|
13368
|
+
findings.push({
|
|
13369
|
+
code: "openapi-compatible-change-review",
|
|
13370
|
+
status: "needs-review",
|
|
13371
|
+
message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
|
|
13372
|
+
});
|
|
13373
|
+
}
|
|
13374
|
+
} catch {
|
|
13375
|
+
findings.push({
|
|
13376
|
+
code: "openapi-diff-not-proven",
|
|
13377
|
+
status: "not-proven",
|
|
13378
|
+
message: "The OpenAPI baseline or current document could not be loaded.",
|
|
13379
|
+
remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
|
|
13380
|
+
});
|
|
13381
|
+
}
|
|
13382
|
+
} else {
|
|
13383
|
+
findings.push({
|
|
13384
|
+
code: "openapi-baseline-not-proven",
|
|
13385
|
+
status: "not-proven",
|
|
13386
|
+
message: "No complete OpenAPI baseline/current pair was supplied.",
|
|
13387
|
+
remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
|
|
13388
|
+
});
|
|
13389
|
+
}
|
|
13390
|
+
findings.push({
|
|
13391
|
+
code: "rendering-mode-preserved",
|
|
13392
|
+
status: "compatible",
|
|
13393
|
+
message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
|
|
13394
|
+
evidence: `selected=${options.renderMode ?? "unspecified"}`
|
|
13395
|
+
});
|
|
13396
|
+
const status = overallStatus(findings);
|
|
13397
|
+
return {
|
|
13398
|
+
version: 1,
|
|
13399
|
+
ok: status === "compatible" || status === "needs-review",
|
|
13400
|
+
status,
|
|
13401
|
+
readOnly: true,
|
|
13402
|
+
writesPerformed: false,
|
|
13403
|
+
rendering: {
|
|
13404
|
+
selected: options.renderMode ?? "unspecified",
|
|
13405
|
+
ssrRequired: false,
|
|
13406
|
+
supportedModes: ["browser", "ssr", "edge", "trusted-server"],
|
|
13407
|
+
guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
|
|
13408
|
+
},
|
|
13409
|
+
dependencies: {
|
|
13410
|
+
expected: migrationDependencies(),
|
|
13411
|
+
problems: dependencyProblems
|
|
13412
|
+
},
|
|
13413
|
+
compiler,
|
|
13414
|
+
artifacts: {
|
|
13415
|
+
contractsManifest,
|
|
13416
|
+
...openApi === undefined ? {} : { openApi }
|
|
13417
|
+
},
|
|
13418
|
+
findings
|
|
13419
|
+
};
|
|
13420
|
+
}
|
|
13421
|
+
function formatMigrationAssessment(result) {
|
|
13422
|
+
return [
|
|
13423
|
+
`Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
|
|
13424
|
+
`Rendering: ${result.rendering.selected}; SSR required: no.`,
|
|
13425
|
+
...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
|
|
13426
|
+
].join(`
|
|
13427
|
+
`);
|
|
13428
|
+
}
|
|
13429
|
+
|
|
13224
13430
|
// src/cli.ts
|
|
13225
13431
|
function isModuleBoundaryPresetName(value) {
|
|
13226
13432
|
return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
|
|
13227
13433
|
}
|
|
13434
|
+
function isMigrationRenderMode(value) {
|
|
13435
|
+
return value === "unspecified" || value === "browser" || value === "ssr" || value === "edge" || value === "trusted-server";
|
|
13436
|
+
}
|
|
13228
13437
|
function printUsage() {
|
|
13229
13438
|
console.log(`
|
|
13230
13439
|
@supacloud/compiler CLI
|
|
@@ -13238,6 +13447,7 @@ Usage:
|
|
|
13238
13447
|
supacloud-compiler context <module> [rootDir] [options]
|
|
13239
13448
|
supacloud-compiler doctor [rootDir] [options]
|
|
13240
13449
|
supacloud-compiler migrate [rootDir] [options]
|
|
13450
|
+
supacloud-compiler migration-assess [rootDir] [options]
|
|
13241
13451
|
supacloud-compiler plan [rootDir] [options]
|
|
13242
13452
|
supacloud-compiler build-delivery [rootDir] [options]
|
|
13243
13453
|
supacloud-compiler openapi-export <openapi-module> <output.json> [options]
|
|
@@ -13255,6 +13465,7 @@ Commands:
|
|
|
13255
13465
|
context Extract an AI-sized module context pack
|
|
13256
13466
|
doctor Run project and generated-artifact health checks
|
|
13257
13467
|
migrate Preview or apply versioned source migrations
|
|
13468
|
+
migration-assess Produce a read-only migration compatibility report
|
|
13258
13469
|
plan Preview deterministic workload targets without writing or deploying
|
|
13259
13470
|
build-delivery Build independent local factories and an atomic delivery manifest (Bun)
|
|
13260
13471
|
openapi-export Export a generated OpenAPI module to a standalone JSON document
|
|
@@ -13278,13 +13489,16 @@ Options:
|
|
|
13278
13489
|
--token-env <name> Environment variable holding the intended user's access token
|
|
13279
13490
|
--check graphql-schema: compare the remote schema without changing the snapshot
|
|
13280
13491
|
--debounce <ms> Debounce source changes in dev mode (default: 100)
|
|
13281
|
-
--json Print machine-readable output for compile/check/graph/explain/context/doctor/plan/build-delivery/openapi-export/openapi-diff
|
|
13492
|
+
--json Print machine-readable output for compile/check/graph/explain/context/doctor/migration-assess/plan/build-delivery/openapi-export/openapi-diff
|
|
13282
13493
|
--space <n> openapi-export: JSON indentation (0-10, default: 2)
|
|
13283
13494
|
--delivery <file> plan/build-delivery: validated JSON configuration (overrides config.delivery)
|
|
13284
13495
|
--dry-run Preview a fix without writing the target file
|
|
13285
13496
|
--write Apply a fix or migration to disk (preview-only by default)
|
|
13286
13497
|
--from-version Migration source-format checkpoint (requires --to-version)
|
|
13287
13498
|
--to-version Migration target checkpoint; verifies installed dependencies
|
|
13499
|
+
--baseline-openapi OpenAPI baseline JSON for migration-assess
|
|
13500
|
+
--current-openapi Current OpenAPI JSON for migration-assess
|
|
13501
|
+
--render-mode Optional browser | ssr | edge | trusted-server label; SSR is never required
|
|
13288
13502
|
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
13289
13503
|
--help, -h Show this help
|
|
13290
13504
|
`);
|
|
@@ -13308,7 +13522,7 @@ async function run() {
|
|
|
13308
13522
|
process.exitCode = 1;
|
|
13309
13523
|
return;
|
|
13310
13524
|
}
|
|
13311
|
-
if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
|
|
13525
|
+
if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "migration-assess", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
|
|
13312
13526
|
console.error(`Error: unknown command "${command}"`);
|
|
13313
13527
|
printUsage();
|
|
13314
13528
|
process.exit(1);
|
|
@@ -13332,6 +13546,9 @@ async function run() {
|
|
|
13332
13546
|
let tokenEnv;
|
|
13333
13547
|
let checkSchema = false;
|
|
13334
13548
|
let deliveryPath;
|
|
13549
|
+
let baselineOpenApi;
|
|
13550
|
+
let currentOpenApi;
|
|
13551
|
+
let renderMode = "unspecified";
|
|
13335
13552
|
const openApiDiffPaths = [];
|
|
13336
13553
|
const openApiExportPaths = [];
|
|
13337
13554
|
let openApiExportSpace;
|
|
@@ -13436,9 +13653,27 @@ async function run() {
|
|
|
13436
13653
|
} else if (arg === "--dry-run") {
|
|
13437
13654
|
dryRun = true;
|
|
13438
13655
|
} else if (arg === "--write") {
|
|
13439
|
-
if (command === "plan")
|
|
13440
|
-
throw new Error(
|
|
13656
|
+
if (command === "plan" || command === "migration-assess")
|
|
13657
|
+
throw new Error(`${command} is read-only; --write is not supported`);
|
|
13441
13658
|
dryRun = false;
|
|
13659
|
+
} else if (arg === "--baseline-openapi" || arg === "--current-openapi") {
|
|
13660
|
+
if (command !== "migration-assess")
|
|
13661
|
+
throw new Error(`${arg} is only supported by migration-assess`);
|
|
13662
|
+
const value = args[++i];
|
|
13663
|
+
if (!value || value.startsWith("-"))
|
|
13664
|
+
throw new Error(`${arg} requires a JSON file path`);
|
|
13665
|
+
if (arg === "--baseline-openapi")
|
|
13666
|
+
baselineOpenApi = value;
|
|
13667
|
+
else
|
|
13668
|
+
currentOpenApi = value;
|
|
13669
|
+
} else if (arg === "--render-mode") {
|
|
13670
|
+
if (command !== "migration-assess")
|
|
13671
|
+
throw new Error("--render-mode is only supported by migration-assess");
|
|
13672
|
+
const value = args[++i];
|
|
13673
|
+
if (!isMigrationRenderMode(value)) {
|
|
13674
|
+
throw new Error("--render-mode requires browser, ssr, edge or trusted-server");
|
|
13675
|
+
}
|
|
13676
|
+
renderMode = value;
|
|
13442
13677
|
} else if (arg === "--from-version" || arg === "--to-version") {
|
|
13443
13678
|
if (command !== "migrate")
|
|
13444
13679
|
throw new Error(`${arg} is only supported by migrate`);
|
|
@@ -13479,7 +13714,7 @@ async function run() {
|
|
|
13479
13714
|
const currentPath = openApiDiffPaths[1];
|
|
13480
13715
|
if (!basePath || !currentPath)
|
|
13481
13716
|
throw new Error("openapi-diff requires two JSON file paths");
|
|
13482
|
-
const result = diffOpenApiDocuments(await readOpenApiJson(
|
|
13717
|
+
const result = diffOpenApiDocuments(await readOpenApiJson(resolve18(process.cwd(), basePath)), await readOpenApiJson(resolve18(process.cwd(), currentPath)));
|
|
13483
13718
|
console.log(json ? JSON.stringify(result, null, 2) : formatOpenApiDiff(result));
|
|
13484
13719
|
if (!result.ok)
|
|
13485
13720
|
process.exitCode = 1;
|
|
@@ -13494,8 +13729,8 @@ async function run() {
|
|
|
13494
13729
|
if (!modulePath || !outputPath)
|
|
13495
13730
|
throw new Error("openapi-export requires an OpenAPI module and output path");
|
|
13496
13731
|
const result = await exportGeneratedOpenApiJson({
|
|
13497
|
-
modulePath:
|
|
13498
|
-
outputPath:
|
|
13732
|
+
modulePath: resolve18(process.cwd(), modulePath),
|
|
13733
|
+
outputPath: resolve18(process.cwd(), outputPath),
|
|
13499
13734
|
...openApiExportSpace === undefined ? {} : { space: openApiExportSpace }
|
|
13500
13735
|
});
|
|
13501
13736
|
console.log(json ? JSON.stringify({ ok: true, ...result }, null, 2) : result.written ? `OpenAPI JSON written: ${result.path}` : `OpenAPI JSON matches: ${result.path}`);
|
|
@@ -13503,7 +13738,7 @@ async function run() {
|
|
|
13503
13738
|
}
|
|
13504
13739
|
if (command === "migrate") {
|
|
13505
13740
|
const result = await migrateProject({
|
|
13506
|
-
rootDir: rootDir ?
|
|
13741
|
+
rootDir: rootDir ? resolve18(process.cwd(), rootDir) : process.cwd(),
|
|
13507
13742
|
write: !dryRun,
|
|
13508
13743
|
...fromVersion === undefined ? {} : { fromVersion },
|
|
13509
13744
|
...toVersion === undefined ? {} : { toVersion }
|
|
@@ -13531,8 +13766,8 @@ async function run() {
|
|
|
13531
13766
|
if (checkSchema && command !== "graphql-schema")
|
|
13532
13767
|
throw new Error("--check is only supported by graphql-schema");
|
|
13533
13768
|
const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
|
|
13534
|
-
const resolvedRoot = rootDir ?
|
|
13535
|
-
const resolvedOut = outDir ?
|
|
13769
|
+
const resolvedRoot = rootDir ? resolve18(process.cwd(), rootDir) : defaults.rootDir;
|
|
13770
|
+
const resolvedOut = outDir ? resolve18(process.cwd(), outDir) : defaults.outDir;
|
|
13536
13771
|
const configured = compileOptionsFromConfig({
|
|
13537
13772
|
...loadedConfig,
|
|
13538
13773
|
root: resolvedRoot,
|
|
@@ -13551,7 +13786,7 @@ async function run() {
|
|
|
13551
13786
|
let delivery = loadedConfig.delivery;
|
|
13552
13787
|
if (deliveryPath !== undefined) {
|
|
13553
13788
|
try {
|
|
13554
|
-
delivery = JSON.parse(await
|
|
13789
|
+
delivery = JSON.parse(await readFile14(resolve18(process.cwd(), deliveryPath), "utf8"));
|
|
13555
13790
|
} catch {
|
|
13556
13791
|
throw new DeliveryConfigurationError;
|
|
13557
13792
|
}
|
|
@@ -13595,10 +13830,21 @@ ${item.suggestion ?? ""}`).join(`
|
|
|
13595
13830
|
console.log(json ? JSON.stringify({ ok: result.upToDate, ...result }, null, 2) : result.written ? `GraphQL schema written: ${result.path}` : result.upToDate ? `GraphQL schema matches: ${result.path}` : `GraphQL schema drift: ${result.path}. Export the role-scoped snapshot and compile before promotion.`);
|
|
13596
13831
|
if (!result.upToDate)
|
|
13597
13832
|
process.exit(1);
|
|
13833
|
+
} else if (command === "migration-assess") {
|
|
13834
|
+
const result = await assessMigration({
|
|
13835
|
+
projectDir: process.cwd(),
|
|
13836
|
+
compile: compileDefaults,
|
|
13837
|
+
...baselineOpenApi === undefined ? {} : { baselineOpenApiPath: baselineOpenApi },
|
|
13838
|
+
...currentOpenApi === undefined ? {} : { currentOpenApiPath: currentOpenApi },
|
|
13839
|
+
renderMode
|
|
13840
|
+
});
|
|
13841
|
+
console.log(json ? JSON.stringify(result, null, 2) : formatMigrationAssessment(result));
|
|
13842
|
+
if (result.status === "breaking" || result.status === "unsupported")
|
|
13843
|
+
process.exitCode = 1;
|
|
13598
13844
|
} else if (command === "fix") {
|
|
13599
13845
|
if (!query)
|
|
13600
13846
|
throw new Error("fix requires a JSON file containing one DiagnosticFix");
|
|
13601
|
-
const fix = JSON.parse(await
|
|
13847
|
+
const fix = JSON.parse(await readFile14(resolve18(process.cwd(), query), "utf8"));
|
|
13602
13848
|
const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
|
|
13603
13849
|
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
13604
13850
|
} else if (command === "compile") {
|
|
@@ -26,6 +26,8 @@ export interface ContractManifest {
|
|
|
26
26
|
permission?: string;
|
|
27
27
|
requestSchemas: Partial<Record<"body" | "params" | "query" | "headers" | "cookie", string>>;
|
|
28
28
|
responseSchema?: string;
|
|
29
|
+
/** Status-aware response schemas; legacy responseSchema remains for compatibility. */
|
|
30
|
+
responseSchemas?: Record<string, string>;
|
|
29
31
|
evidence?: string;
|
|
30
32
|
}>;
|
|
31
33
|
permissions: string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { AppliedDiagnosticFix, ApplyDiagnosticFixOptions } from "./fixes";
|
|
|
11
11
|
export { checkProject, compileProject } from "./compile";
|
|
12
12
|
export { watchProject } from "./watch";
|
|
13
13
|
export { migrateProject, SUPACLOUD_MIGRATIONS, migrateRouteResponse } from "./migrations";
|
|
14
|
+
export { assessMigration, formatMigrationAssessment } from "./migration-assess";
|
|
15
|
+
export type { MigrationAssessmentFinding, MigrationAssessmentOptions, MigrationAssessmentResult, MigrationAssessmentStatus, MigrationRenderMode, } from "./migration-assess";
|
|
14
16
|
export { createContextPack, createExecutionPlans, doctorProject, explainGraph, formatGraph, exportGraphDot, exportGraphMermaid, } from "./inspect";
|
|
15
17
|
export { createIncrementalCompiler } from "./incremental";
|
|
16
18
|
export { createDependencyGraphCache } from "./incremental";
|