@supacloud/compiler 0.23.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 +384 -48
- package/dist/contract-manifest.d.ts +53 -0
- package/dist/generate.d.ts +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1042 -742
- package/dist/migration-assess.d.ts +62 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -78,6 +78,86 @@ function findClosestMatch(target, candidates) {
|
|
|
78
78
|
}
|
|
79
79
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
80
80
|
|
|
81
|
+
// src/contract-manifest.ts
|
|
82
|
+
function buildContractManifest(graph, artifacts) {
|
|
83
|
+
const commands = [];
|
|
84
|
+
const queries = [];
|
|
85
|
+
const routes = [];
|
|
86
|
+
const permissions = new Set;
|
|
87
|
+
const rpc = [];
|
|
88
|
+
const events = [];
|
|
89
|
+
const fixtures = new Set;
|
|
90
|
+
for (const module of graph.modules) {
|
|
91
|
+
for (const command of module.commands) {
|
|
92
|
+
if (command.permission)
|
|
93
|
+
permissions.add(command.permission);
|
|
94
|
+
if (command.rpc)
|
|
95
|
+
rpc.push({ command: command.name, adapter: command.rpc });
|
|
96
|
+
if (command.audit)
|
|
97
|
+
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
98
|
+
commands.push({
|
|
99
|
+
name: command.name,
|
|
100
|
+
className: command.className,
|
|
101
|
+
module: module.name,
|
|
102
|
+
...command.permission === undefined ? {} : { permission: command.permission },
|
|
103
|
+
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
104
|
+
transaction: command.transaction,
|
|
105
|
+
idempotency: command.idempotency,
|
|
106
|
+
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (const query of module.queries) {
|
|
110
|
+
queries.push({ name: query.name, className: query.className, module: module.name });
|
|
111
|
+
}
|
|
112
|
+
for (const controller of module.controllers) {
|
|
113
|
+
for (const route of controller.routes) {
|
|
114
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
115
|
+
if (command?.permission)
|
|
116
|
+
permissions.add(command.permission);
|
|
117
|
+
if (route.contract?.evidence)
|
|
118
|
+
fixtures.add(route.contract.evidence);
|
|
119
|
+
routes.push({
|
|
120
|
+
method: route.method,
|
|
121
|
+
path: joinRoutePaths(controller.path, route.path),
|
|
122
|
+
module: module.name,
|
|
123
|
+
controller: controller.className,
|
|
124
|
+
handler: route.handler,
|
|
125
|
+
...route.command === undefined ? {} : { command: route.command },
|
|
126
|
+
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
127
|
+
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
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
|
+
},
|
|
132
|
+
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
138
|
+
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
139
|
+
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
140
|
+
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
141
|
+
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
142
|
+
return {
|
|
143
|
+
version: 1,
|
|
144
|
+
commands,
|
|
145
|
+
queries,
|
|
146
|
+
routes,
|
|
147
|
+
permissions: [...permissions].sort(),
|
|
148
|
+
rpc,
|
|
149
|
+
events,
|
|
150
|
+
fixtures: [...fixtures].sort(),
|
|
151
|
+
artifacts: {
|
|
152
|
+
...artifacts,
|
|
153
|
+
sdk: "generated-client",
|
|
154
|
+
openapiDocument: "generated",
|
|
155
|
+
permissionManifest: "generated"
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var init_contract_manifest = () => {};
|
|
160
|
+
|
|
81
161
|
// src/generate.ts
|
|
82
162
|
import { createHash as createHash4 } from "node:crypto";
|
|
83
163
|
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -219,9 +299,16 @@ function renderApplication(graph, options) {
|
|
|
219
299
|
const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
|
|
220
300
|
const openApiCode = options.generateOpenApi ? renderOpenApi(graph, options) : undefined;
|
|
221
301
|
const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
|
|
302
|
+
const contractsManifest = buildContractManifest(graph, {
|
|
303
|
+
client: clientCode !== undefined,
|
|
304
|
+
openapi: openApiCode !== undefined,
|
|
305
|
+
permissions: permissionsCode !== undefined
|
|
306
|
+
});
|
|
222
307
|
return {
|
|
223
308
|
applicationCode: code,
|
|
224
309
|
manifestJson: JSON.stringify(manifest, null, 2) + `
|
|
310
|
+
`,
|
|
311
|
+
contractsManifestJson: JSON.stringify(contractsManifest, null, 2) + `
|
|
225
312
|
`,
|
|
226
313
|
...clientCode === undefined ? {} : { clientCode },
|
|
227
314
|
...openApiCode === undefined ? {} : { openApiCode },
|
|
@@ -233,9 +320,11 @@ async function generateApplication(graph, options) {
|
|
|
233
320
|
await mkdir(options.outDir, { recursive: true });
|
|
234
321
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
235
322
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
323
|
+
const contractsManifestPath = join2(options.outDir, "contracts.manifest.json");
|
|
236
324
|
const writeCandidates = [
|
|
237
325
|
{ path: applicationPath, content: rendered.applicationCode },
|
|
238
|
-
{ path: manifestPath, content: rendered.manifestJson }
|
|
326
|
+
{ path: manifestPath, content: rendered.manifestJson },
|
|
327
|
+
{ path: contractsManifestPath, content: rendered.contractsManifestJson }
|
|
239
328
|
];
|
|
240
329
|
if (rendered.clientCode) {
|
|
241
330
|
writeCandidates.push({ path: join2(options.outDir, "client.ts"), content: rendered.clientCode });
|
|
@@ -2096,7 +2185,9 @@ function destroyScopeInstances(
|
|
|
2096
2185
|
scopeDestructions.set(scope, destruction);
|
|
2097
2186
|
return destruction;
|
|
2098
2187
|
}`;
|
|
2099
|
-
var init_generate = () => {
|
|
2188
|
+
var init_generate = __esm(() => {
|
|
2189
|
+
init_contract_manifest();
|
|
2190
|
+
});
|
|
2100
2191
|
|
|
2101
2192
|
// src/graphql-client.ts
|
|
2102
2193
|
var GRAPHQL_CLIENT_SOURCE = `
|
|
@@ -2609,12 +2700,12 @@ var init_graphql = __esm(() => {
|
|
|
2609
2700
|
});
|
|
2610
2701
|
|
|
2611
2702
|
// src/database-contracts.ts
|
|
2612
|
-
import { createHash as
|
|
2613
|
-
import { mkdir as mkdir5, readFile as
|
|
2614
|
-
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";
|
|
2615
2706
|
import * as ts13 from "@typescript/typescript6";
|
|
2616
2707
|
function hash2(value) {
|
|
2617
|
-
return
|
|
2708
|
+
return createHash10("sha256").update(value).digest("hex");
|
|
2618
2709
|
}
|
|
2619
2710
|
function importPath(out, path) {
|
|
2620
2711
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2639,19 +2730,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2639
2730
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2640
2731
|
}
|
|
2641
2732
|
return {
|
|
2642
|
-
rootDir:
|
|
2643
|
-
outDir:
|
|
2644
|
-
postgrestTypes:
|
|
2645
|
-
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")),
|
|
2646
2737
|
role: field("role"),
|
|
2647
|
-
graphql: { ...graphql, schema:
|
|
2648
|
-
migrations: migrations.map((file) =>
|
|
2738
|
+
graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
|
|
2739
|
+
migrations: migrations.map((file) => resolve16(directory, file))
|
|
2649
2740
|
};
|
|
2650
2741
|
}
|
|
2651
2742
|
async function generateDatabaseContracts(options, check = false) {
|
|
2652
|
-
const rootDir =
|
|
2653
|
-
const postgrestTypes =
|
|
2654
|
-
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");
|
|
2655
2746
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2656
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));
|
|
2657
2748
|
if (!database)
|
|
@@ -2678,8 +2769,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2678
2769
|
}
|
|
2679
2770
|
const inputs = {};
|
|
2680
2771
|
const addInput = async (path) => {
|
|
2681
|
-
const absolute =
|
|
2682
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2772
|
+
const absolute = resolve16(rootDir, path);
|
|
2773
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile12(absolute, "utf8"));
|
|
2683
2774
|
};
|
|
2684
2775
|
await addInput(postgrestTypes);
|
|
2685
2776
|
await addInput(drizzleSchema);
|
|
@@ -2695,8 +2786,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2695
2786
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2696
2787
|
await addInput(source.fileName);
|
|
2697
2788
|
}
|
|
2698
|
-
await addInput(
|
|
2699
|
-
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) {
|
|
2700
2791
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2701
2792
|
}
|
|
2702
2793
|
for (const path of options.migrations)
|
|
@@ -2717,17 +2808,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2717
2808
|
version: 1,
|
|
2718
2809
|
role: options.role,
|
|
2719
2810
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2720
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2811
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
|
|
2721
2812
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2722
2813
|
};
|
|
2723
2814
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2724
2815
|
`;
|
|
2725
2816
|
const mismatches = [];
|
|
2726
2817
|
for (const [file, content] of Object.entries(files)) {
|
|
2727
|
-
const path =
|
|
2818
|
+
const path = resolve16(outDir, file);
|
|
2728
2819
|
let current;
|
|
2729
2820
|
try {
|
|
2730
|
-
current = await
|
|
2821
|
+
current = await readFile12(path, "utf8");
|
|
2731
2822
|
} catch (error) {
|
|
2732
2823
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2733
2824
|
throw error;
|
|
@@ -2738,13 +2829,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2738
2829
|
if (!check) {
|
|
2739
2830
|
await mkdir5(outDir, { recursive: true });
|
|
2740
2831
|
for (const [file, content] of Object.entries(files))
|
|
2741
|
-
await writeFileIfChanged(
|
|
2832
|
+
await writeFileIfChanged(resolve16(outDir, file), content);
|
|
2742
2833
|
}
|
|
2743
2834
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2744
2835
|
}
|
|
2745
2836
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2746
|
-
const absolute =
|
|
2747
|
-
const value = JSON.parse(await
|
|
2837
|
+
const absolute = resolve16(path);
|
|
2838
|
+
const value = JSON.parse(await readFile12(absolute, "utf8"));
|
|
2748
2839
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname10(absolute)), check);
|
|
2749
2840
|
}
|
|
2750
2841
|
var init_database_contracts = __esm(() => {
|
|
@@ -2754,9 +2845,9 @@ var init_database_contracts = __esm(() => {
|
|
|
2754
2845
|
});
|
|
2755
2846
|
|
|
2756
2847
|
// src/graphql-schema.ts
|
|
2757
|
-
import { mkdir as mkdir6, readFile as
|
|
2758
|
-
import { createHash as
|
|
2759
|
-
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";
|
|
2760
2851
|
async function pullGraphqlSchema(options) {
|
|
2761
2852
|
assertGraphqlOptions({ schema: options.output });
|
|
2762
2853
|
const endpoint = new URL(options.url);
|
|
@@ -2796,7 +2887,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2796
2887
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2797
2888
|
}
|
|
2798
2889
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2799
|
-
const path =
|
|
2890
|
+
const path = resolve17(options.output);
|
|
2800
2891
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2801
2892
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2802
2893
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2804,13 +2895,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2804
2895
|
`;
|
|
2805
2896
|
let previous;
|
|
2806
2897
|
try {
|
|
2807
|
-
previous = await
|
|
2898
|
+
previous = await readFile13(path, "utf8");
|
|
2808
2899
|
} catch (error) {
|
|
2809
2900
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2810
2901
|
throw error;
|
|
2811
2902
|
}
|
|
2812
2903
|
const upToDate = previous === content;
|
|
2813
|
-
const schemaHash =
|
|
2904
|
+
const schemaHash = createHash11("sha256").update(content).digest("hex");
|
|
2814
2905
|
if (options.check)
|
|
2815
2906
|
return { path, schemaHash, upToDate, written: false };
|
|
2816
2907
|
await mkdir6(dirname11(path), { recursive: true });
|
|
@@ -2823,8 +2914,8 @@ var init_graphql_schema = __esm(() => {
|
|
|
2823
2914
|
});
|
|
2824
2915
|
|
|
2825
2916
|
// src/cli.ts
|
|
2826
|
-
import { resolve as
|
|
2827
|
-
import { readFile as
|
|
2917
|
+
import { resolve as resolve18 } from "node:path";
|
|
2918
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
2828
2919
|
|
|
2829
2920
|
// src/analyze.ts
|
|
2830
2921
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -6985,6 +7076,7 @@ async function compileProject(options) {
|
|
|
6985
7076
|
"client.ts": rendered.clientCode,
|
|
6986
7077
|
"openapi.ts": rendered.openApiCode,
|
|
6987
7078
|
"permissions.ts": rendered.permissionsCode,
|
|
7079
|
+
"contracts.manifest.json": rendered.contractsManifestJson,
|
|
6988
7080
|
"graphql.ts": graphql.files["graphql.ts"],
|
|
6989
7081
|
"graphql.documents.ts": graphql.files["graphql.documents.ts"]
|
|
6990
7082
|
}, options.strict ?? false));
|
|
@@ -7054,7 +7146,8 @@ async function checkProject(options) {
|
|
|
7054
7146
|
const expectedFiles = {
|
|
7055
7147
|
...graphql.files,
|
|
7056
7148
|
"application.ts": rendered.applicationCode,
|
|
7057
|
-
"app.manifest.json": rendered.manifestJson
|
|
7149
|
+
"app.manifest.json": rendered.manifestJson,
|
|
7150
|
+
"contracts.manifest.json": rendered.contractsManifestJson
|
|
7058
7151
|
};
|
|
7059
7152
|
if (rendered.clientCode) {
|
|
7060
7153
|
expectedFiles["client.ts"] = rendered.clientCode;
|
|
@@ -7521,6 +7614,8 @@ function optionsKeyOf(options) {
|
|
|
7521
7614
|
requireRouteContracts: options.requireRouteContracts,
|
|
7522
7615
|
detectOrphanModules: options.detectOrphanModules,
|
|
7523
7616
|
generateClient: options.generateClient,
|
|
7617
|
+
generateOpenApi: options.generateOpenApi,
|
|
7618
|
+
openApi: options.openApi,
|
|
7524
7619
|
generatePermissions: options.generatePermissions,
|
|
7525
7620
|
typeSafety: options.typeSafety,
|
|
7526
7621
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -7655,6 +7750,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
7655
7750
|
|
|
7656
7751
|
// src/watch.ts
|
|
7657
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
|
+
}
|
|
7658
7763
|
function watchProject(options) {
|
|
7659
7764
|
const rootDir = resolve6(options.rootDir);
|
|
7660
7765
|
const outDir = resolve6(options.outDir);
|
|
@@ -7747,7 +7852,7 @@ function watchProject(options) {
|
|
|
7747
7852
|
const relativePath = relative7(outDir, changedPath);
|
|
7748
7853
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
7749
7854
|
return;
|
|
7750
|
-
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)) {
|
|
7751
7856
|
schedule(relative7(rootDir, changedPath));
|
|
7752
7857
|
}
|
|
7753
7858
|
});
|
|
@@ -12205,7 +12310,7 @@ function compilerVersion() {
|
|
|
12205
12310
|
}
|
|
12206
12311
|
function migrationDependencies() {
|
|
12207
12312
|
return {
|
|
12208
|
-
"@supacloud/app": "0.
|
|
12313
|
+
"@supacloud/app": "0.16.0",
|
|
12209
12314
|
"@supacloud/compiler": compilerVersion(),
|
|
12210
12315
|
"@supacloud/elysia": "0.18.0",
|
|
12211
12316
|
elysia: "1.4.30",
|
|
@@ -12626,6 +12731,11 @@ async function migrateProject(options) {
|
|
|
12626
12731
|
};
|
|
12627
12732
|
}
|
|
12628
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
|
+
|
|
12629
12739
|
// src/openapi-tools.ts
|
|
12630
12740
|
import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
12631
12741
|
import { dirname as dirname9, resolve as resolve14 } from "node:path";
|
|
@@ -13131,10 +13241,199 @@ function formatOpenApiDiff(result) {
|
|
|
13131
13241
|
`);
|
|
13132
13242
|
}
|
|
13133
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
|
+
|
|
13134
13430
|
// src/cli.ts
|
|
13135
13431
|
function isModuleBoundaryPresetName(value) {
|
|
13136
13432
|
return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
|
|
13137
13433
|
}
|
|
13434
|
+
function isMigrationRenderMode(value) {
|
|
13435
|
+
return value === "unspecified" || value === "browser" || value === "ssr" || value === "edge" || value === "trusted-server";
|
|
13436
|
+
}
|
|
13138
13437
|
function printUsage() {
|
|
13139
13438
|
console.log(`
|
|
13140
13439
|
@supacloud/compiler CLI
|
|
@@ -13148,6 +13447,7 @@ Usage:
|
|
|
13148
13447
|
supacloud-compiler context <module> [rootDir] [options]
|
|
13149
13448
|
supacloud-compiler doctor [rootDir] [options]
|
|
13150
13449
|
supacloud-compiler migrate [rootDir] [options]
|
|
13450
|
+
supacloud-compiler migration-assess [rootDir] [options]
|
|
13151
13451
|
supacloud-compiler plan [rootDir] [options]
|
|
13152
13452
|
supacloud-compiler build-delivery [rootDir] [options]
|
|
13153
13453
|
supacloud-compiler openapi-export <openapi-module> <output.json> [options]
|
|
@@ -13165,6 +13465,7 @@ Commands:
|
|
|
13165
13465
|
context Extract an AI-sized module context pack
|
|
13166
13466
|
doctor Run project and generated-artifact health checks
|
|
13167
13467
|
migrate Preview or apply versioned source migrations
|
|
13468
|
+
migration-assess Produce a read-only migration compatibility report
|
|
13168
13469
|
plan Preview deterministic workload targets without writing or deploying
|
|
13169
13470
|
build-delivery Build independent local factories and an atomic delivery manifest (Bun)
|
|
13170
13471
|
openapi-export Export a generated OpenAPI module to a standalone JSON document
|
|
@@ -13188,13 +13489,16 @@ Options:
|
|
|
13188
13489
|
--token-env <name> Environment variable holding the intended user's access token
|
|
13189
13490
|
--check graphql-schema: compare the remote schema without changing the snapshot
|
|
13190
13491
|
--debounce <ms> Debounce source changes in dev mode (default: 100)
|
|
13191
|
-
--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
|
|
13192
13493
|
--space <n> openapi-export: JSON indentation (0-10, default: 2)
|
|
13193
13494
|
--delivery <file> plan/build-delivery: validated JSON configuration (overrides config.delivery)
|
|
13194
13495
|
--dry-run Preview a fix without writing the target file
|
|
13195
13496
|
--write Apply a fix or migration to disk (preview-only by default)
|
|
13196
13497
|
--from-version Migration source-format checkpoint (requires --to-version)
|
|
13197
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
|
|
13198
13502
|
--preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
|
|
13199
13503
|
--help, -h Show this help
|
|
13200
13504
|
`);
|
|
@@ -13218,7 +13522,7 @@ async function run() {
|
|
|
13218
13522
|
process.exitCode = 1;
|
|
13219
13523
|
return;
|
|
13220
13524
|
}
|
|
13221
|
-
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)) {
|
|
13222
13526
|
console.error(`Error: unknown command "${command}"`);
|
|
13223
13527
|
printUsage();
|
|
13224
13528
|
process.exit(1);
|
|
@@ -13242,6 +13546,9 @@ async function run() {
|
|
|
13242
13546
|
let tokenEnv;
|
|
13243
13547
|
let checkSchema = false;
|
|
13244
13548
|
let deliveryPath;
|
|
13549
|
+
let baselineOpenApi;
|
|
13550
|
+
let currentOpenApi;
|
|
13551
|
+
let renderMode = "unspecified";
|
|
13245
13552
|
const openApiDiffPaths = [];
|
|
13246
13553
|
const openApiExportPaths = [];
|
|
13247
13554
|
let openApiExportSpace;
|
|
@@ -13346,9 +13653,27 @@ async function run() {
|
|
|
13346
13653
|
} else if (arg === "--dry-run") {
|
|
13347
13654
|
dryRun = true;
|
|
13348
13655
|
} else if (arg === "--write") {
|
|
13349
|
-
if (command === "plan")
|
|
13350
|
-
throw new Error(
|
|
13656
|
+
if (command === "plan" || command === "migration-assess")
|
|
13657
|
+
throw new Error(`${command} is read-only; --write is not supported`);
|
|
13351
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;
|
|
13352
13677
|
} else if (arg === "--from-version" || arg === "--to-version") {
|
|
13353
13678
|
if (command !== "migrate")
|
|
13354
13679
|
throw new Error(`${arg} is only supported by migrate`);
|
|
@@ -13389,7 +13714,7 @@ async function run() {
|
|
|
13389
13714
|
const currentPath = openApiDiffPaths[1];
|
|
13390
13715
|
if (!basePath || !currentPath)
|
|
13391
13716
|
throw new Error("openapi-diff requires two JSON file paths");
|
|
13392
|
-
const result = diffOpenApiDocuments(await readOpenApiJson(
|
|
13717
|
+
const result = diffOpenApiDocuments(await readOpenApiJson(resolve18(process.cwd(), basePath)), await readOpenApiJson(resolve18(process.cwd(), currentPath)));
|
|
13393
13718
|
console.log(json ? JSON.stringify(result, null, 2) : formatOpenApiDiff(result));
|
|
13394
13719
|
if (!result.ok)
|
|
13395
13720
|
process.exitCode = 1;
|
|
@@ -13404,8 +13729,8 @@ async function run() {
|
|
|
13404
13729
|
if (!modulePath || !outputPath)
|
|
13405
13730
|
throw new Error("openapi-export requires an OpenAPI module and output path");
|
|
13406
13731
|
const result = await exportGeneratedOpenApiJson({
|
|
13407
|
-
modulePath:
|
|
13408
|
-
outputPath:
|
|
13732
|
+
modulePath: resolve18(process.cwd(), modulePath),
|
|
13733
|
+
outputPath: resolve18(process.cwd(), outputPath),
|
|
13409
13734
|
...openApiExportSpace === undefined ? {} : { space: openApiExportSpace }
|
|
13410
13735
|
});
|
|
13411
13736
|
console.log(json ? JSON.stringify({ ok: true, ...result }, null, 2) : result.written ? `OpenAPI JSON written: ${result.path}` : `OpenAPI JSON matches: ${result.path}`);
|
|
@@ -13413,7 +13738,7 @@ async function run() {
|
|
|
13413
13738
|
}
|
|
13414
13739
|
if (command === "migrate") {
|
|
13415
13740
|
const result = await migrateProject({
|
|
13416
|
-
rootDir: rootDir ?
|
|
13741
|
+
rootDir: rootDir ? resolve18(process.cwd(), rootDir) : process.cwd(),
|
|
13417
13742
|
write: !dryRun,
|
|
13418
13743
|
...fromVersion === undefined ? {} : { fromVersion },
|
|
13419
13744
|
...toVersion === undefined ? {} : { toVersion }
|
|
@@ -13441,8 +13766,8 @@ async function run() {
|
|
|
13441
13766
|
if (checkSchema && command !== "graphql-schema")
|
|
13442
13767
|
throw new Error("--check is only supported by graphql-schema");
|
|
13443
13768
|
const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
|
|
13444
|
-
const resolvedRoot = rootDir ?
|
|
13445
|
-
const resolvedOut = outDir ?
|
|
13769
|
+
const resolvedRoot = rootDir ? resolve18(process.cwd(), rootDir) : defaults.rootDir;
|
|
13770
|
+
const resolvedOut = outDir ? resolve18(process.cwd(), outDir) : defaults.outDir;
|
|
13446
13771
|
const configured = compileOptionsFromConfig({
|
|
13447
13772
|
...loadedConfig,
|
|
13448
13773
|
root: resolvedRoot,
|
|
@@ -13461,7 +13786,7 @@ async function run() {
|
|
|
13461
13786
|
let delivery = loadedConfig.delivery;
|
|
13462
13787
|
if (deliveryPath !== undefined) {
|
|
13463
13788
|
try {
|
|
13464
|
-
delivery = JSON.parse(await
|
|
13789
|
+
delivery = JSON.parse(await readFile14(resolve18(process.cwd(), deliveryPath), "utf8"));
|
|
13465
13790
|
} catch {
|
|
13466
13791
|
throw new DeliveryConfigurationError;
|
|
13467
13792
|
}
|
|
@@ -13505,10 +13830,21 @@ ${item.suggestion ?? ""}`).join(`
|
|
|
13505
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.`);
|
|
13506
13831
|
if (!result.upToDate)
|
|
13507
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;
|
|
13508
13844
|
} else if (command === "fix") {
|
|
13509
13845
|
if (!query)
|
|
13510
13846
|
throw new Error("fix requires a JSON file containing one DiagnosticFix");
|
|
13511
|
-
const fix = JSON.parse(await
|
|
13847
|
+
const fix = JSON.parse(await readFile14(resolve18(process.cwd(), query), "utf8"));
|
|
13512
13848
|
const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
|
|
13513
13849
|
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
13514
13850
|
} else if (command === "compile") {
|