@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/index.js
CHANGED
|
@@ -77,6 +77,86 @@ function findClosestMatch(target, candidates) {
|
|
|
77
77
|
}
|
|
78
78
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
79
79
|
|
|
80
|
+
// src/contract-manifest.ts
|
|
81
|
+
function buildContractManifest(graph, artifacts) {
|
|
82
|
+
const commands = [];
|
|
83
|
+
const queries = [];
|
|
84
|
+
const routes = [];
|
|
85
|
+
const permissions = new Set;
|
|
86
|
+
const rpc = [];
|
|
87
|
+
const events = [];
|
|
88
|
+
const fixtures = new Set;
|
|
89
|
+
for (const module of graph.modules) {
|
|
90
|
+
for (const command of module.commands) {
|
|
91
|
+
if (command.permission)
|
|
92
|
+
permissions.add(command.permission);
|
|
93
|
+
if (command.rpc)
|
|
94
|
+
rpc.push({ command: command.name, adapter: command.rpc });
|
|
95
|
+
if (command.audit)
|
|
96
|
+
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
97
|
+
commands.push({
|
|
98
|
+
name: command.name,
|
|
99
|
+
className: command.className,
|
|
100
|
+
module: module.name,
|
|
101
|
+
...command.permission === undefined ? {} : { permission: command.permission },
|
|
102
|
+
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
103
|
+
transaction: command.transaction,
|
|
104
|
+
idempotency: command.idempotency,
|
|
105
|
+
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const query of module.queries) {
|
|
109
|
+
queries.push({ name: query.name, className: query.className, module: module.name });
|
|
110
|
+
}
|
|
111
|
+
for (const controller of module.controllers) {
|
|
112
|
+
for (const route of controller.routes) {
|
|
113
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
114
|
+
if (command?.permission)
|
|
115
|
+
permissions.add(command.permission);
|
|
116
|
+
if (route.contract?.evidence)
|
|
117
|
+
fixtures.add(route.contract.evidence);
|
|
118
|
+
routes.push({
|
|
119
|
+
method: route.method,
|
|
120
|
+
path: joinRoutePaths(controller.path, route.path),
|
|
121
|
+
module: module.name,
|
|
122
|
+
controller: controller.className,
|
|
123
|
+
handler: route.handler,
|
|
124
|
+
...route.command === undefined ? {} : { command: route.command },
|
|
125
|
+
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
126
|
+
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
127
|
+
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
128
|
+
...route.responses === undefined ? {} : {
|
|
129
|
+
responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
|
|
130
|
+
},
|
|
131
|
+
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
137
|
+
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
138
|
+
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
139
|
+
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
140
|
+
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
141
|
+
return {
|
|
142
|
+
version: 1,
|
|
143
|
+
commands,
|
|
144
|
+
queries,
|
|
145
|
+
routes,
|
|
146
|
+
permissions: [...permissions].sort(),
|
|
147
|
+
rpc,
|
|
148
|
+
events,
|
|
149
|
+
fixtures: [...fixtures].sort(),
|
|
150
|
+
artifacts: {
|
|
151
|
+
...artifacts,
|
|
152
|
+
sdk: "generated-client",
|
|
153
|
+
openapiDocument: "generated",
|
|
154
|
+
permissionManifest: "generated"
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
var init_contract_manifest = () => {};
|
|
159
|
+
|
|
80
160
|
// src/generate.ts
|
|
81
161
|
import { createHash as createHash4 } from "node:crypto";
|
|
82
162
|
import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
@@ -218,9 +298,16 @@ function renderApplication(graph, options) {
|
|
|
218
298
|
const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
|
|
219
299
|
const openApiCode = options.generateOpenApi ? renderOpenApi(graph, options) : undefined;
|
|
220
300
|
const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
|
|
301
|
+
const contractsManifest = buildContractManifest(graph, {
|
|
302
|
+
client: clientCode !== undefined,
|
|
303
|
+
openapi: openApiCode !== undefined,
|
|
304
|
+
permissions: permissionsCode !== undefined
|
|
305
|
+
});
|
|
221
306
|
return {
|
|
222
307
|
applicationCode: code,
|
|
223
308
|
manifestJson: JSON.stringify(manifest, null, 2) + `
|
|
309
|
+
`,
|
|
310
|
+
contractsManifestJson: JSON.stringify(contractsManifest, null, 2) + `
|
|
224
311
|
`,
|
|
225
312
|
...clientCode === undefined ? {} : { clientCode },
|
|
226
313
|
...openApiCode === undefined ? {} : { openApiCode },
|
|
@@ -232,9 +319,11 @@ async function generateApplication(graph, options) {
|
|
|
232
319
|
await mkdir(options.outDir, { recursive: true });
|
|
233
320
|
const applicationPath = join2(options.outDir, "application.ts");
|
|
234
321
|
const manifestPath = join2(options.outDir, "app.manifest.json");
|
|
322
|
+
const contractsManifestPath = join2(options.outDir, "contracts.manifest.json");
|
|
235
323
|
const writeCandidates = [
|
|
236
324
|
{ path: applicationPath, content: rendered.applicationCode },
|
|
237
|
-
{ path: manifestPath, content: rendered.manifestJson }
|
|
325
|
+
{ path: manifestPath, content: rendered.manifestJson },
|
|
326
|
+
{ path: contractsManifestPath, content: rendered.contractsManifestJson }
|
|
238
327
|
];
|
|
239
328
|
if (rendered.clientCode) {
|
|
240
329
|
writeCandidates.push({ path: join2(options.outDir, "client.ts"), content: rendered.clientCode });
|
|
@@ -2095,7 +2184,9 @@ function destroyScopeInstances(
|
|
|
2095
2184
|
scopeDestructions.set(scope, destruction);
|
|
2096
2185
|
return destruction;
|
|
2097
2186
|
}`;
|
|
2098
|
-
var init_generate = () => {
|
|
2187
|
+
var init_generate = __esm(() => {
|
|
2188
|
+
init_contract_manifest();
|
|
2189
|
+
});
|
|
2099
2190
|
|
|
2100
2191
|
// src/graphql-client.ts
|
|
2101
2192
|
var GRAPHQL_CLIENT_SOURCE = `
|
|
@@ -2608,9 +2699,9 @@ var init_graphql = __esm(() => {
|
|
|
2608
2699
|
});
|
|
2609
2700
|
|
|
2610
2701
|
// src/graphql-schema.ts
|
|
2611
|
-
import { mkdir as mkdir5, readFile as
|
|
2612
|
-
import { createHash as
|
|
2613
|
-
import { dirname as dirname10, resolve as
|
|
2702
|
+
import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
|
|
2703
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
2704
|
+
import { dirname as dirname10, resolve as resolve16 } from "node:path";
|
|
2614
2705
|
async function pullGraphqlSchema(options) {
|
|
2615
2706
|
assertGraphqlOptions({ schema: options.output });
|
|
2616
2707
|
const endpoint = new URL(options.url);
|
|
@@ -2650,7 +2741,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2650
2741
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2651
2742
|
}
|
|
2652
2743
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2653
|
-
const path =
|
|
2744
|
+
const path = resolve16(options.output);
|
|
2654
2745
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2655
2746
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2656
2747
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2658,13 +2749,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2658
2749
|
`;
|
|
2659
2750
|
let previous;
|
|
2660
2751
|
try {
|
|
2661
|
-
previous = await
|
|
2752
|
+
previous = await readFile12(path, "utf8");
|
|
2662
2753
|
} catch (error) {
|
|
2663
2754
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2664
2755
|
throw error;
|
|
2665
2756
|
}
|
|
2666
2757
|
const upToDate = previous === content;
|
|
2667
|
-
const schemaHash =
|
|
2758
|
+
const schemaHash = createHash10("sha256").update(content).digest("hex");
|
|
2668
2759
|
if (options.check)
|
|
2669
2760
|
return { path, schemaHash, upToDate, written: false };
|
|
2670
2761
|
await mkdir5(dirname10(path), { recursive: true });
|
|
@@ -2677,12 +2768,12 @@ var init_graphql_schema = __esm(() => {
|
|
|
2677
2768
|
});
|
|
2678
2769
|
|
|
2679
2770
|
// src/database-contracts.ts
|
|
2680
|
-
import { createHash as
|
|
2681
|
-
import { mkdir as mkdir6, readFile as
|
|
2682
|
-
import { dirname as dirname11, relative as relative12, resolve as
|
|
2771
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
2772
|
+
import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
|
|
2773
|
+
import { dirname as dirname11, relative as relative12, resolve as resolve17 } from "node:path";
|
|
2683
2774
|
import * as ts13 from "@typescript/typescript6";
|
|
2684
2775
|
function hash2(value) {
|
|
2685
|
-
return
|
|
2776
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
2686
2777
|
}
|
|
2687
2778
|
function importPath(out, path) {
|
|
2688
2779
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2707,19 +2798,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2707
2798
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2708
2799
|
}
|
|
2709
2800
|
return {
|
|
2710
|
-
rootDir:
|
|
2711
|
-
outDir:
|
|
2712
|
-
postgrestTypes:
|
|
2713
|
-
drizzleSchema:
|
|
2801
|
+
rootDir: resolve17(directory, field("rootDir")),
|
|
2802
|
+
outDir: resolve17(directory, field("outDir")),
|
|
2803
|
+
postgrestTypes: resolve17(directory, field("postgrestTypes")),
|
|
2804
|
+
drizzleSchema: resolve17(directory, field("drizzleSchema")),
|
|
2714
2805
|
role: field("role"),
|
|
2715
|
-
graphql: { ...graphql, schema:
|
|
2716
|
-
migrations: migrations.map((file) =>
|
|
2806
|
+
graphql: { ...graphql, schema: resolve17(directory, graphql.schema) },
|
|
2807
|
+
migrations: migrations.map((file) => resolve17(directory, file))
|
|
2717
2808
|
};
|
|
2718
2809
|
}
|
|
2719
2810
|
async function generateDatabaseContracts(options, check = false) {
|
|
2720
|
-
const rootDir =
|
|
2721
|
-
const postgrestTypes =
|
|
2722
|
-
const snapshot = await
|
|
2811
|
+
const rootDir = resolve17(options.rootDir), outDir = resolve17(options.outDir);
|
|
2812
|
+
const postgrestTypes = resolve17(rootDir, options.postgrestTypes), drizzleSchema = resolve17(rootDir, options.drizzleSchema);
|
|
2813
|
+
const snapshot = await readFile13(postgrestTypes, "utf8");
|
|
2723
2814
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2724
2815
|
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));
|
|
2725
2816
|
if (!database)
|
|
@@ -2746,8 +2837,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2746
2837
|
}
|
|
2747
2838
|
const inputs = {};
|
|
2748
2839
|
const addInput = async (path) => {
|
|
2749
|
-
const absolute =
|
|
2750
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2840
|
+
const absolute = resolve17(rootDir, path);
|
|
2841
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile13(absolute, "utf8"));
|
|
2751
2842
|
};
|
|
2752
2843
|
await addInput(postgrestTypes);
|
|
2753
2844
|
await addInput(drizzleSchema);
|
|
@@ -2763,8 +2854,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2763
2854
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2764
2855
|
await addInput(source.fileName);
|
|
2765
2856
|
}
|
|
2766
|
-
await addInput(
|
|
2767
|
-
if (new Set(options.migrations.map((path) =>
|
|
2857
|
+
await addInput(resolve17(rootDir, options.graphql.schema));
|
|
2858
|
+
if (new Set(options.migrations.map((path) => resolve17(rootDir, path))).size !== options.migrations.length) {
|
|
2768
2859
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2769
2860
|
}
|
|
2770
2861
|
for (const path of options.migrations)
|
|
@@ -2785,17 +2876,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2785
2876
|
version: 1,
|
|
2786
2877
|
role: options.role,
|
|
2787
2878
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2788
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2879
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve17(rootDir, path)).replaceAll("\\", "/")),
|
|
2789
2880
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2790
2881
|
};
|
|
2791
2882
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2792
2883
|
`;
|
|
2793
2884
|
const mismatches = [];
|
|
2794
2885
|
for (const [file, content] of Object.entries(files)) {
|
|
2795
|
-
const path =
|
|
2886
|
+
const path = resolve17(outDir, file);
|
|
2796
2887
|
let current;
|
|
2797
2888
|
try {
|
|
2798
|
-
current = await
|
|
2889
|
+
current = await readFile13(path, "utf8");
|
|
2799
2890
|
} catch (error) {
|
|
2800
2891
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2801
2892
|
throw error;
|
|
@@ -2806,13 +2897,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2806
2897
|
if (!check) {
|
|
2807
2898
|
await mkdir6(outDir, { recursive: true });
|
|
2808
2899
|
for (const [file, content] of Object.entries(files))
|
|
2809
|
-
await writeFileIfChanged(
|
|
2900
|
+
await writeFileIfChanged(resolve17(outDir, file), content);
|
|
2810
2901
|
}
|
|
2811
2902
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2812
2903
|
}
|
|
2813
2904
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2814
|
-
const absolute =
|
|
2815
|
-
const value = JSON.parse(await
|
|
2905
|
+
const absolute = resolve17(path);
|
|
2906
|
+
const value = JSON.parse(await readFile13(absolute, "utf8"));
|
|
2816
2907
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname11(absolute)), check);
|
|
2817
2908
|
}
|
|
2818
2909
|
var init_database_contracts = __esm(() => {
|
|
@@ -6982,6 +7073,7 @@ async function compileProject(options) {
|
|
|
6982
7073
|
"client.ts": rendered.clientCode,
|
|
6983
7074
|
"openapi.ts": rendered.openApiCode,
|
|
6984
7075
|
"permissions.ts": rendered.permissionsCode,
|
|
7076
|
+
"contracts.manifest.json": rendered.contractsManifestJson,
|
|
6985
7077
|
"graphql.ts": graphql.files["graphql.ts"],
|
|
6986
7078
|
"graphql.documents.ts": graphql.files["graphql.documents.ts"]
|
|
6987
7079
|
}, options.strict ?? false));
|
|
@@ -7051,7 +7143,8 @@ async function checkProject(options) {
|
|
|
7051
7143
|
const expectedFiles = {
|
|
7052
7144
|
...graphql.files,
|
|
7053
7145
|
"application.ts": rendered.applicationCode,
|
|
7054
|
-
"app.manifest.json": rendered.manifestJson
|
|
7146
|
+
"app.manifest.json": rendered.manifestJson,
|
|
7147
|
+
"contracts.manifest.json": rendered.contractsManifestJson
|
|
7055
7148
|
};
|
|
7056
7149
|
if (rendered.clientCode) {
|
|
7057
7150
|
expectedFiles["client.ts"] = rendered.clientCode;
|
|
@@ -11495,6 +11588,8 @@ function optionsKeyOf(options) {
|
|
|
11495
11588
|
requireRouteContracts: options.requireRouteContracts,
|
|
11496
11589
|
detectOrphanModules: options.detectOrphanModules,
|
|
11497
11590
|
generateClient: options.generateClient,
|
|
11591
|
+
generateOpenApi: options.generateOpenApi,
|
|
11592
|
+
openApi: options.openApi,
|
|
11498
11593
|
generatePermissions: options.generatePermissions,
|
|
11499
11594
|
typeSafety: options.typeSafety,
|
|
11500
11595
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -11629,6 +11724,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
11629
11724
|
|
|
11630
11725
|
// src/watch.ts
|
|
11631
11726
|
var DEFAULT_DEBOUNCE_MS = 100;
|
|
11727
|
+
function isCompilerConfigurationPath(rootDir, changedPath) {
|
|
11728
|
+
const relativePath = relative9(rootDir, changedPath).split(sep9).join("/");
|
|
11729
|
+
return [
|
|
11730
|
+
"supacloud.config.ts",
|
|
11731
|
+
"supacloud.config.mts",
|
|
11732
|
+
"supacloud.config.js",
|
|
11733
|
+
"supacloud.config.mjs",
|
|
11734
|
+
"tsconfig.json"
|
|
11735
|
+
].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
|
|
11736
|
+
}
|
|
11632
11737
|
function watchProject(options) {
|
|
11633
11738
|
const rootDir = resolve10(options.rootDir);
|
|
11634
11739
|
const outDir = resolve10(options.outDir);
|
|
@@ -11721,7 +11826,7 @@ function watchProject(options) {
|
|
|
11721
11826
|
const relativePath = relative9(outDir, changedPath);
|
|
11722
11827
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
11723
11828
|
return;
|
|
11724
|
-
if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
11829
|
+
if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
|
|
11725
11830
|
schedule(relative9(rootDir, changedPath));
|
|
11726
11831
|
}
|
|
11727
11832
|
});
|
|
@@ -11775,7 +11880,7 @@ function compilerVersion() {
|
|
|
11775
11880
|
}
|
|
11776
11881
|
function migrationDependencies() {
|
|
11777
11882
|
return {
|
|
11778
|
-
"@supacloud/app": "0.
|
|
11883
|
+
"@supacloud/app": "0.16.0",
|
|
11779
11884
|
"@supacloud/compiler": compilerVersion(),
|
|
11780
11885
|
"@supacloud/elysia": "0.18.0",
|
|
11781
11886
|
elysia: "1.4.30",
|
|
@@ -12195,797 +12300,989 @@ async function migrateProject(options) {
|
|
|
12195
12300
|
issues
|
|
12196
12301
|
};
|
|
12197
12302
|
}
|
|
12198
|
-
// src/
|
|
12199
|
-
import {
|
|
12200
|
-
import {
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12303
|
+
// src/migration-assess.ts
|
|
12304
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
12305
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
12306
|
+
import { join as join7, resolve as resolve14 } from "node:path";
|
|
12307
|
+
|
|
12308
|
+
// src/openapi-tools.ts
|
|
12309
|
+
import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
12310
|
+
import { dirname as dirname8, resolve as resolve13 } from "node:path";
|
|
12311
|
+
import { pathToFileURL } from "node:url";
|
|
12312
|
+
|
|
12313
|
+
class OpenApiDocumentError extends Error {
|
|
12314
|
+
code = "OPENAPI_DOCUMENT_INVALID";
|
|
12315
|
+
constructor() {
|
|
12316
|
+
super("OpenAPI document is invalid or could not be loaded.");
|
|
12317
|
+
this.name = "OpenApiDocumentError";
|
|
12210
12318
|
}
|
|
12211
|
-
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
12212
|
-
return lines.join(`
|
|
12213
|
-
`);
|
|
12214
12319
|
}
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
12320
|
+
var HTTP_METHODS = [
|
|
12321
|
+
"get",
|
|
12322
|
+
"put",
|
|
12323
|
+
"post",
|
|
12324
|
+
"delete",
|
|
12325
|
+
"options",
|
|
12326
|
+
"head",
|
|
12327
|
+
"patch",
|
|
12328
|
+
"trace"
|
|
12329
|
+
];
|
|
12330
|
+
var COMPONENT_GROUPS = [
|
|
12331
|
+
"schemas",
|
|
12332
|
+
"responses",
|
|
12333
|
+
"parameters",
|
|
12334
|
+
"requestBodies",
|
|
12335
|
+
"headers",
|
|
12336
|
+
"securitySchemes"
|
|
12337
|
+
];
|
|
12338
|
+
function isRecord(value) {
|
|
12339
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12236
12340
|
}
|
|
12237
|
-
function
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12341
|
+
function stringValue(value) {
|
|
12342
|
+
return typeof value === "string" ? value : undefined;
|
|
12343
|
+
}
|
|
12344
|
+
function recordValue(value) {
|
|
12345
|
+
return isRecord(value) ? value : undefined;
|
|
12346
|
+
}
|
|
12347
|
+
function arrayValue(value) {
|
|
12348
|
+
return Array.isArray(value) ? value : [];
|
|
12349
|
+
}
|
|
12350
|
+
function sortedKeys(value) {
|
|
12351
|
+
return value ? Object.keys(value).sort() : [];
|
|
12352
|
+
}
|
|
12353
|
+
function sameJson(left, right) {
|
|
12354
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
12355
|
+
}
|
|
12356
|
+
function decodeJsonPointerPart(value) {
|
|
12357
|
+
return value.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12358
|
+
}
|
|
12359
|
+
function resolveSchema(value, document, seen = new Set) {
|
|
12360
|
+
const schema = recordValue(value);
|
|
12361
|
+
if (!schema)
|
|
12362
|
+
return;
|
|
12363
|
+
const ref = stringValue(schema.$ref);
|
|
12364
|
+
if (!ref || !ref.startsWith("#/components/schemas/"))
|
|
12365
|
+
return schema;
|
|
12366
|
+
if (seen.has(ref))
|
|
12367
|
+
return schema;
|
|
12368
|
+
const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
|
|
12369
|
+
const components = recordValue(document.components);
|
|
12370
|
+
const schemas = recordValue(components?.schemas);
|
|
12371
|
+
const target = schemas?.[name];
|
|
12372
|
+
if (target === undefined)
|
|
12373
|
+
return schema;
|
|
12374
|
+
return resolveSchema(target, document, new Set([...seen, ref]));
|
|
12375
|
+
}
|
|
12376
|
+
function schemaEnum(value) {
|
|
12377
|
+
return Array.isArray(value?.enum) ? value.enum : undefined;
|
|
12378
|
+
}
|
|
12379
|
+
function schemaProperties(value) {
|
|
12380
|
+
return recordValue(value?.properties);
|
|
12381
|
+
}
|
|
12382
|
+
function schemaRequired(value) {
|
|
12383
|
+
return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
|
|
12384
|
+
}
|
|
12385
|
+
function schemaType(value) {
|
|
12386
|
+
return value?.type;
|
|
12387
|
+
}
|
|
12388
|
+
function addChange(changes, change) {
|
|
12389
|
+
if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
|
|
12390
|
+
return;
|
|
12391
|
+
changes.push(change);
|
|
12392
|
+
}
|
|
12393
|
+
function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
|
|
12394
|
+
const baseSchema = resolveSchema(baseValue, baseDocument);
|
|
12395
|
+
const currentSchema = resolveSchema(currentValue, currentDocument);
|
|
12396
|
+
if (!baseSchema || !currentSchema)
|
|
12397
|
+
return;
|
|
12398
|
+
const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
|
|
12399
|
+
if (seen.has(pair))
|
|
12400
|
+
return;
|
|
12401
|
+
seen.add(pair);
|
|
12402
|
+
if (schemaType(baseSchema) !== undefined && schemaType(currentSchema) !== undefined && !sameJson(schemaType(baseSchema), schemaType(currentSchema))) {
|
|
12403
|
+
addChange(changes, {
|
|
12404
|
+
kind: "breaking",
|
|
12405
|
+
code: "schema-type-changed",
|
|
12406
|
+
path,
|
|
12407
|
+
message: "The schema type changed."
|
|
12408
|
+
});
|
|
12241
12409
|
}
|
|
12242
|
-
const
|
|
12243
|
-
const
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12410
|
+
const baseEnum = schemaEnum(baseSchema);
|
|
12411
|
+
const currentEnum = schemaEnum(currentSchema);
|
|
12412
|
+
if (baseEnum && currentEnum) {
|
|
12413
|
+
for (const value of baseEnum) {
|
|
12414
|
+
if (!currentEnum.some((candidate) => sameJson(candidate, value))) {
|
|
12415
|
+
addChange(changes, {
|
|
12416
|
+
kind: "breaking",
|
|
12417
|
+
code: "schema-enum-value-removed",
|
|
12418
|
+
path,
|
|
12419
|
+
message: "An enum value accepted by the previous contract was removed."
|
|
12420
|
+
});
|
|
12421
|
+
break;
|
|
12422
|
+
}
|
|
12252
12423
|
}
|
|
12253
12424
|
}
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
if (!visited.has(neighbor) && byName.has(neighbor)) {
|
|
12268
|
-
visited.add(neighbor);
|
|
12269
|
-
selected.add(neighbor);
|
|
12270
|
-
queue.push(neighbor);
|
|
12425
|
+
const baseProperties = schemaProperties(baseSchema);
|
|
12426
|
+
const currentProperties = schemaProperties(currentSchema);
|
|
12427
|
+
if (baseProperties && currentProperties) {
|
|
12428
|
+
for (const name of sortedKeys(baseProperties)) {
|
|
12429
|
+
if (currentProperties[name] === undefined) {
|
|
12430
|
+
const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
|
|
12431
|
+
if (direction === "output" || inputRemovalIsBreaking) {
|
|
12432
|
+
addChange(changes, {
|
|
12433
|
+
kind: "breaking",
|
|
12434
|
+
code: direction === "output" ? "response-property-removed" : "request-property-removed",
|
|
12435
|
+
path: `${path}.properties.${name}`,
|
|
12436
|
+
message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
|
|
12437
|
+
});
|
|
12271
12438
|
}
|
|
12439
|
+
continue;
|
|
12272
12440
|
}
|
|
12441
|
+
compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
|
|
12273
12442
|
}
|
|
12274
12443
|
}
|
|
12275
|
-
const
|
|
12276
|
-
const
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
...module.providers.map((provider) => provider.file),
|
|
12288
|
-
...module.controllers.map((controller) => controller.file),
|
|
12289
|
-
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
12290
|
-
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
12291
|
-
const referencedTokens = new Set;
|
|
12292
|
-
for (const module of modules) {
|
|
12293
|
-
for (const provider of module.providers) {
|
|
12294
|
-
for (const token of provider.deps)
|
|
12295
|
-
referencedTokens.add(token);
|
|
12296
|
-
}
|
|
12297
|
-
for (const controller of module.controllers) {
|
|
12298
|
-
for (const token of controller.deps)
|
|
12299
|
-
referencedTokens.add(token);
|
|
12444
|
+
const baseRequired = schemaRequired(baseSchema);
|
|
12445
|
+
const currentRequired = schemaRequired(currentSchema);
|
|
12446
|
+
if (direction === "input") {
|
|
12447
|
+
for (const name of currentRequired) {
|
|
12448
|
+
if (!baseRequired.has(name)) {
|
|
12449
|
+
addChange(changes, {
|
|
12450
|
+
kind: "breaking",
|
|
12451
|
+
code: "request-property-required",
|
|
12452
|
+
path: `${path}.required`,
|
|
12453
|
+
message: `Request property '${name}' became required.`
|
|
12454
|
+
});
|
|
12455
|
+
}
|
|
12300
12456
|
}
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
executionPlans: createExecutionPlans({ ...graph, modules }),
|
|
12309
|
-
routeContracts: inspectRouteContracts({ ...graph, modules }),
|
|
12310
|
-
...graphql ? { graphql } : {},
|
|
12311
|
-
diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
|
|
12312
|
-
relatedModules: {
|
|
12313
|
-
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
12314
|
-
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
12457
|
+
if (baseSchema.additionalProperties !== false && currentSchema.additionalProperties === false) {
|
|
12458
|
+
addChange(changes, {
|
|
12459
|
+
kind: "breaking",
|
|
12460
|
+
code: "request-additional-properties-rejected",
|
|
12461
|
+
path,
|
|
12462
|
+
message: "The request schema now rejects additional properties."
|
|
12463
|
+
});
|
|
12315
12464
|
}
|
|
12316
|
-
}
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
}
|
|
12326
|
-
function createExecutionPlans(graph) {
|
|
12327
|
-
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
12328
|
-
return graph.modules.flatMap((module) => [
|
|
12329
|
-
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
12330
|
-
const command = module.commands.find((item) => item.className === route.command);
|
|
12331
|
-
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
12332
|
-
return {
|
|
12333
|
-
module: module.name,
|
|
12334
|
-
kind: "route",
|
|
12335
|
-
name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
|
|
12336
|
-
...command ? { command: command.name } : {},
|
|
12337
|
-
stages: [
|
|
12338
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12339
|
-
...aspects("route", route.aspects),
|
|
12340
|
-
...aspects("command", command?.aspects),
|
|
12341
|
-
...command ? [
|
|
12342
|
-
"commandExecutor",
|
|
12343
|
-
"authorize",
|
|
12344
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12345
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12346
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12347
|
-
]
|
|
12348
|
-
] : [],
|
|
12349
|
-
"handler",
|
|
12350
|
-
...command?.audit && !command.rpc ? ["audit"] : []
|
|
12351
|
-
]
|
|
12352
|
-
};
|
|
12353
|
-
})),
|
|
12354
|
-
...module.commands.map((command) => ({
|
|
12355
|
-
module: module.name,
|
|
12356
|
-
kind: "command",
|
|
12357
|
-
name: command.name,
|
|
12358
|
-
command: command.name,
|
|
12359
|
-
stages: [
|
|
12360
|
-
"authorize",
|
|
12361
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12362
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12363
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12364
|
-
],
|
|
12365
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12366
|
-
...aspects("command", command.aspects),
|
|
12367
|
-
"handler",
|
|
12368
|
-
...command.audit && !command.rpc ? ["audit"] : []
|
|
12369
|
-
]
|
|
12370
|
-
})),
|
|
12371
|
-
...(module.jobs ?? []).map((job) => ({
|
|
12372
|
-
module: module.name,
|
|
12373
|
-
kind: "job",
|
|
12374
|
-
name: job.name,
|
|
12375
|
-
stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
|
|
12376
|
-
}))
|
|
12377
|
-
]);
|
|
12378
|
-
}
|
|
12379
|
-
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
12380
|
-
const checks = [
|
|
12381
|
-
{
|
|
12382
|
-
name: "project-root",
|
|
12383
|
-
ok: existsSync4(rootDir),
|
|
12384
|
-
detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
12385
|
-
},
|
|
12386
|
-
{
|
|
12387
|
-
name: "tsconfig",
|
|
12388
|
-
ok: existsSync4(join7(rootDir, "tsconfig.json")),
|
|
12389
|
-
detail: existsSync4(join7(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
12390
|
-
},
|
|
12391
|
-
{
|
|
12392
|
-
name: "modules",
|
|
12393
|
-
ok: graph.modules.length > 0,
|
|
12394
|
-
detail: `${graph.modules.length} module(s) discovered`
|
|
12395
|
-
},
|
|
12396
|
-
{
|
|
12397
|
-
name: "generated-artifacts",
|
|
12398
|
-
ok: upToDate,
|
|
12399
|
-
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
12465
|
+
} else {
|
|
12466
|
+
for (const name of baseRequired) {
|
|
12467
|
+
if (!currentRequired.has(name)) {
|
|
12468
|
+
addChange(changes, {
|
|
12469
|
+
kind: "breaking",
|
|
12470
|
+
code: "response-property-optional",
|
|
12471
|
+
path: `${path}.required`,
|
|
12472
|
+
message: `Response property '${name}' is no longer guaranteed.`
|
|
12473
|
+
});
|
|
12474
|
+
}
|
|
12400
12475
|
}
|
|
12401
|
-
];
|
|
12402
|
-
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
12403
|
-
return {
|
|
12404
|
-
checks,
|
|
12405
|
-
diagnostics: allDiagnostics,
|
|
12406
|
-
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
12407
|
-
};
|
|
12408
|
-
}
|
|
12409
|
-
function explainModule(graph, module) {
|
|
12410
|
-
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
12411
|
-
return [
|
|
12412
|
-
`MODULE ${module.name}`,
|
|
12413
|
-
` file: ${module.file}:${module.line}`,
|
|
12414
|
-
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
12415
|
-
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
12416
|
-
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
12417
|
-
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
12418
|
-
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
|
|
12419
|
-
...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
|
|
12420
|
-
].join(`
|
|
12421
|
-
`);
|
|
12422
|
-
}
|
|
12423
|
-
function explainProvider(graph, module, provider) {
|
|
12424
|
-
const dependents = graph.modules.flatMap((candidate) => [
|
|
12425
|
-
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
12426
|
-
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
12427
|
-
]);
|
|
12428
|
-
return [
|
|
12429
|
-
`PROVIDER ${provider.token}`,
|
|
12430
|
-
` module: ${module.name}`,
|
|
12431
|
-
` file: ${provider.file}:${provider.line}`,
|
|
12432
|
-
` kind: ${provider.kind}`,
|
|
12433
|
-
` scope: ${provider.scope}`,
|
|
12434
|
-
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
12435
|
-
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
12436
|
-
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
12437
|
-
].join(`
|
|
12438
|
-
`);
|
|
12439
|
-
}
|
|
12440
|
-
function findProvider(graph, subject) {
|
|
12441
|
-
for (const module of graph.modules) {
|
|
12442
|
-
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
12443
|
-
if (provider)
|
|
12444
|
-
return { module, provider };
|
|
12445
12476
|
}
|
|
12446
|
-
return;
|
|
12447
12477
|
}
|
|
12448
|
-
function
|
|
12449
|
-
const
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12478
|
+
function parameterKey(value) {
|
|
12479
|
+
const parameter = recordValue(value);
|
|
12480
|
+
const name = stringValue(parameter?.name);
|
|
12481
|
+
const location = stringValue(parameter?.in);
|
|
12482
|
+
return name && location ? `${location}:${name}` : undefined;
|
|
12483
|
+
}
|
|
12484
|
+
function operationParameters(pathItem, operation) {
|
|
12485
|
+
const result = new Map;
|
|
12486
|
+
for (const source of [pathItem.parameters, operation.parameters]) {
|
|
12487
|
+
for (const item of arrayValue(source)) {
|
|
12488
|
+
const parameter = recordValue(item);
|
|
12489
|
+
const key = parameterKey(parameter);
|
|
12490
|
+
if (parameter && key)
|
|
12491
|
+
result.set(key, parameter);
|
|
12456
12492
|
}
|
|
12457
12493
|
}
|
|
12458
|
-
return
|
|
12459
|
-
`);
|
|
12494
|
+
return result;
|
|
12460
12495
|
}
|
|
12461
|
-
function
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
for (const
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12496
|
+
function parameterSchema(value) {
|
|
12497
|
+
return value.schema;
|
|
12498
|
+
}
|
|
12499
|
+
function compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12500
|
+
const baseParameters = operationParameters(basePathItem, baseOperation);
|
|
12501
|
+
const currentParameters = operationParameters(currentPathItem, currentOperation);
|
|
12502
|
+
for (const key of [...baseParameters.keys()].sort()) {
|
|
12503
|
+
const baseParameter = baseParameters.get(key);
|
|
12504
|
+
const currentParameter = currentParameters.get(key);
|
|
12505
|
+
if (!baseParameter || !currentParameter) {
|
|
12506
|
+
addChange(changes, {
|
|
12507
|
+
kind: "breaking",
|
|
12508
|
+
code: "parameter-removed",
|
|
12509
|
+
path: `${path}.parameters.${key}`,
|
|
12510
|
+
message: "A parameter from the previous contract was removed."
|
|
12511
|
+
});
|
|
12512
|
+
continue;
|
|
12513
|
+
}
|
|
12514
|
+
if (currentParameter.required === true && baseParameter.required !== true) {
|
|
12515
|
+
addChange(changes, {
|
|
12516
|
+
kind: "breaking",
|
|
12517
|
+
code: "parameter-required",
|
|
12518
|
+
path: `${path}.parameters.${key}`,
|
|
12519
|
+
message: "An optional parameter became required."
|
|
12520
|
+
});
|
|
12521
|
+
}
|
|
12522
|
+
if (parameterSchema(baseParameter) !== undefined && parameterSchema(currentParameter) !== undefined) {
|
|
12523
|
+
compareSchema(parameterSchema(baseParameter), parameterSchema(currentParameter), baseDocument, currentDocument, `${path}.parameters.${key}.schema`, "input", changes);
|
|
12471
12524
|
}
|
|
12472
12525
|
}
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
class OpenApiDocumentError extends Error {
|
|
12487
|
-
code = "OPENAPI_DOCUMENT_INVALID";
|
|
12488
|
-
constructor() {
|
|
12489
|
-
super("OpenAPI document is invalid or could not be loaded.");
|
|
12490
|
-
this.name = "OpenApiDocumentError";
|
|
12526
|
+
for (const key of [...currentParameters.keys()].sort()) {
|
|
12527
|
+
if (baseParameters.has(key))
|
|
12528
|
+
continue;
|
|
12529
|
+
const parameter = currentParameters.get(key);
|
|
12530
|
+
if (!parameter)
|
|
12531
|
+
continue;
|
|
12532
|
+
addChange(changes, {
|
|
12533
|
+
kind: parameter.required === true ? "breaking" : "non-breaking",
|
|
12534
|
+
code: parameter.required === true ? "parameter-required" : "parameter-added",
|
|
12535
|
+
path: `${path}.parameters.${key}`,
|
|
12536
|
+
message: parameter.required === true ? "A new required parameter was added." : "An optional parameter was added."
|
|
12537
|
+
});
|
|
12491
12538
|
}
|
|
12492
12539
|
}
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
}
|
|
12520
|
-
function arrayValue(value) {
|
|
12521
|
-
return Array.isArray(value) ? value : [];
|
|
12522
|
-
}
|
|
12523
|
-
function sortedKeys(value) {
|
|
12524
|
-
return value ? Object.keys(value).sort() : [];
|
|
12525
|
-
}
|
|
12526
|
-
function sameJson(left, right) {
|
|
12527
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
12528
|
-
}
|
|
12529
|
-
function decodeJsonPointerPart(value) {
|
|
12530
|
-
return value.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12531
|
-
}
|
|
12532
|
-
function resolveSchema(value, document, seen = new Set) {
|
|
12533
|
-
const schema = recordValue(value);
|
|
12534
|
-
if (!schema)
|
|
12535
|
-
return;
|
|
12536
|
-
const ref = stringValue(schema.$ref);
|
|
12537
|
-
if (!ref || !ref.startsWith("#/components/schemas/"))
|
|
12538
|
-
return schema;
|
|
12539
|
-
if (seen.has(ref))
|
|
12540
|
-
return schema;
|
|
12541
|
-
const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
|
|
12542
|
-
const components = recordValue(document.components);
|
|
12543
|
-
const schemas = recordValue(components?.schemas);
|
|
12544
|
-
const target = schemas?.[name];
|
|
12545
|
-
if (target === undefined)
|
|
12546
|
-
return schema;
|
|
12547
|
-
return resolveSchema(target, document, new Set([...seen, ref]));
|
|
12548
|
-
}
|
|
12549
|
-
function schemaEnum(value) {
|
|
12550
|
-
return Array.isArray(value?.enum) ? value.enum : undefined;
|
|
12551
|
-
}
|
|
12552
|
-
function schemaProperties(value) {
|
|
12553
|
-
return recordValue(value?.properties);
|
|
12554
|
-
}
|
|
12555
|
-
function schemaRequired(value) {
|
|
12556
|
-
return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
|
|
12557
|
-
}
|
|
12558
|
-
function schemaType(value) {
|
|
12559
|
-
return value?.type;
|
|
12560
|
-
}
|
|
12561
|
-
function addChange(changes, change) {
|
|
12562
|
-
if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
|
|
12563
|
-
return;
|
|
12564
|
-
changes.push(change);
|
|
12565
|
-
}
|
|
12566
|
-
function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
|
|
12567
|
-
const baseSchema = resolveSchema(baseValue, baseDocument);
|
|
12568
|
-
const currentSchema = resolveSchema(currentValue, currentDocument);
|
|
12569
|
-
if (!baseSchema || !currentSchema)
|
|
12570
|
-
return;
|
|
12571
|
-
const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
|
|
12572
|
-
if (seen.has(pair))
|
|
12540
|
+
function compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12541
|
+
const baseBody = recordValue(baseOperation.requestBody);
|
|
12542
|
+
const currentBody = recordValue(currentOperation.requestBody);
|
|
12543
|
+
if (!baseBody || !currentBody) {
|
|
12544
|
+
if (baseBody && !currentBody) {
|
|
12545
|
+
addChange(changes, {
|
|
12546
|
+
kind: "breaking",
|
|
12547
|
+
code: "request-body-removed",
|
|
12548
|
+
path: `${path}.requestBody`,
|
|
12549
|
+
message: "A request body from the previous contract was removed."
|
|
12550
|
+
});
|
|
12551
|
+
} else if (!baseBody && currentBody?.required === true) {
|
|
12552
|
+
addChange(changes, {
|
|
12553
|
+
kind: "breaking",
|
|
12554
|
+
code: "request-body-required",
|
|
12555
|
+
path: `${path}.requestBody`,
|
|
12556
|
+
message: "A request body became required."
|
|
12557
|
+
});
|
|
12558
|
+
} else if (!baseBody && currentBody) {
|
|
12559
|
+
addChange(changes, {
|
|
12560
|
+
kind: "non-breaking",
|
|
12561
|
+
code: "request-body-added",
|
|
12562
|
+
path: `${path}.requestBody`,
|
|
12563
|
+
message: "An optional request body was added."
|
|
12564
|
+
});
|
|
12565
|
+
}
|
|
12573
12566
|
return;
|
|
12574
|
-
|
|
12575
|
-
if (
|
|
12567
|
+
}
|
|
12568
|
+
if (currentBody.required === true && baseBody.required !== true) {
|
|
12576
12569
|
addChange(changes, {
|
|
12577
12570
|
kind: "breaking",
|
|
12578
|
-
code: "
|
|
12579
|
-
path
|
|
12580
|
-
message: "
|
|
12571
|
+
code: "request-body-required",
|
|
12572
|
+
path: `${path}.requestBody`,
|
|
12573
|
+
message: "An optional request body became required."
|
|
12581
12574
|
});
|
|
12582
12575
|
}
|
|
12583
|
-
const
|
|
12584
|
-
const
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12576
|
+
const baseContent = recordValue(baseBody.content);
|
|
12577
|
+
const currentContent = recordValue(currentBody.content);
|
|
12578
|
+
for (const mediaType of sortedKeys(baseContent)) {
|
|
12579
|
+
const baseMedia = recordValue(baseContent?.[mediaType]);
|
|
12580
|
+
const currentMedia = recordValue(currentContent?.[mediaType]);
|
|
12581
|
+
if (!baseMedia || !currentMedia) {
|
|
12582
|
+
addChange(changes, {
|
|
12583
|
+
kind: "breaking",
|
|
12584
|
+
code: "request-media-type-removed",
|
|
12585
|
+
path: `${path}.requestBody.content.${mediaType}`,
|
|
12586
|
+
message: "A request media type from the previous contract was removed."
|
|
12587
|
+
});
|
|
12588
|
+
continue;
|
|
12596
12589
|
}
|
|
12597
|
-
|
|
12598
|
-
|
|
12599
|
-
const currentProperties = schemaProperties(currentSchema);
|
|
12600
|
-
if (baseProperties && currentProperties) {
|
|
12601
|
-
for (const name of sortedKeys(baseProperties)) {
|
|
12602
|
-
if (currentProperties[name] === undefined) {
|
|
12603
|
-
const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
|
|
12604
|
-
if (direction === "output" || inputRemovalIsBreaking) {
|
|
12605
|
-
addChange(changes, {
|
|
12606
|
-
kind: "breaking",
|
|
12607
|
-
code: direction === "output" ? "response-property-removed" : "request-property-removed",
|
|
12608
|
-
path: `${path}.properties.${name}`,
|
|
12609
|
-
message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
|
|
12610
|
-
});
|
|
12611
|
-
}
|
|
12612
|
-
continue;
|
|
12613
|
-
}
|
|
12614
|
-
compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
|
|
12590
|
+
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12591
|
+
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
|
|
12615
12592
|
}
|
|
12616
12593
|
}
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12594
|
+
}
|
|
12595
|
+
function compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12596
|
+
const baseResponses = recordValue(baseOperation.responses);
|
|
12597
|
+
const currentResponses = recordValue(currentOperation.responses);
|
|
12598
|
+
for (const status of sortedKeys(baseResponses)) {
|
|
12599
|
+
const baseResponse = recordValue(baseResponses?.[status]);
|
|
12600
|
+
const currentResponse = recordValue(currentResponses?.[status]);
|
|
12601
|
+
if (!baseResponse || !currentResponse) {
|
|
12602
|
+
addChange(changes, {
|
|
12603
|
+
kind: "breaking",
|
|
12604
|
+
code: "response-removed",
|
|
12605
|
+
path: `${path}.responses.${status}`,
|
|
12606
|
+
message: "A response status from the previous contract was removed."
|
|
12607
|
+
});
|
|
12608
|
+
continue;
|
|
12609
|
+
}
|
|
12610
|
+
const baseContent = recordValue(baseResponse.content);
|
|
12611
|
+
const currentContent = recordValue(currentResponse.content);
|
|
12612
|
+
for (const mediaType of sortedKeys(baseContent)) {
|
|
12613
|
+
const baseMedia = recordValue(baseContent?.[mediaType]);
|
|
12614
|
+
const currentMedia = recordValue(currentContent?.[mediaType]);
|
|
12615
|
+
if (!baseMedia || !currentMedia) {
|
|
12622
12616
|
addChange(changes, {
|
|
12623
|
-
kind: "breaking",
|
|
12624
|
-
code: "
|
|
12625
|
-
path: `${path}.
|
|
12626
|
-
message:
|
|
12617
|
+
kind: "breaking",
|
|
12618
|
+
code: "response-media-type-removed",
|
|
12619
|
+
path: `${path}.responses.${status}.content.${mediaType}`,
|
|
12620
|
+
message: "A response media type from the previous contract was removed."
|
|
12627
12621
|
});
|
|
12622
|
+
continue;
|
|
12623
|
+
}
|
|
12624
|
+
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12625
|
+
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.responses.${status}.content.${mediaType}.schema`, "output", changes);
|
|
12628
12626
|
}
|
|
12629
12627
|
}
|
|
12630
|
-
|
|
12628
|
+
}
|
|
12629
|
+
for (const status of sortedKeys(currentResponses)) {
|
|
12630
|
+
if (baseResponses?.[status] !== undefined)
|
|
12631
|
+
continue;
|
|
12632
|
+
addChange(changes, {
|
|
12633
|
+
kind: "non-breaking",
|
|
12634
|
+
code: "response-added",
|
|
12635
|
+
path: `${path}.responses.${status}`,
|
|
12636
|
+
message: "A response status was added."
|
|
12637
|
+
});
|
|
12638
|
+
}
|
|
12639
|
+
}
|
|
12640
|
+
function compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
|
|
12641
|
+
compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12642
|
+
compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12643
|
+
compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
|
|
12644
|
+
const baseSecurity = baseOperation.security;
|
|
12645
|
+
const currentSecurity = currentOperation.security;
|
|
12646
|
+
if (Array.isArray(currentSecurity) && currentSecurity.length > 0 && (!Array.isArray(baseSecurity) || baseSecurity.length === 0)) {
|
|
12647
|
+
addChange(changes, {
|
|
12648
|
+
kind: "breaking",
|
|
12649
|
+
code: "security-requirement-added",
|
|
12650
|
+
path: `${path}.security`,
|
|
12651
|
+
message: "The operation now requires authentication or an additional security scheme."
|
|
12652
|
+
});
|
|
12653
|
+
}
|
|
12654
|
+
}
|
|
12655
|
+
function compareComponents(baseDocument, currentDocument, changes) {
|
|
12656
|
+
const baseComponents = recordValue(baseDocument.components);
|
|
12657
|
+
const currentComponents = recordValue(currentDocument.components);
|
|
12658
|
+
for (const group of COMPONENT_GROUPS) {
|
|
12659
|
+
const baseGroup = recordValue(baseComponents?.[group]);
|
|
12660
|
+
const currentGroup = recordValue(currentComponents?.[group]);
|
|
12661
|
+
for (const name of sortedKeys(baseGroup)) {
|
|
12662
|
+
if (currentGroup?.[name] !== undefined)
|
|
12663
|
+
continue;
|
|
12631
12664
|
addChange(changes, {
|
|
12632
12665
|
kind: "breaking",
|
|
12633
|
-
code: "
|
|
12634
|
-
path
|
|
12635
|
-
message: "
|
|
12666
|
+
code: "component-removed",
|
|
12667
|
+
path: `components.${group}.${name}`,
|
|
12668
|
+
message: "A reusable component from the previous contract was removed."
|
|
12636
12669
|
});
|
|
12637
12670
|
}
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
}
|
|
12671
|
+
for (const name of sortedKeys(currentGroup)) {
|
|
12672
|
+
if (baseGroup?.[name] !== undefined)
|
|
12673
|
+
continue;
|
|
12674
|
+
addChange(changes, {
|
|
12675
|
+
kind: "non-breaking",
|
|
12676
|
+
code: "component-added",
|
|
12677
|
+
path: `components.${group}.${name}`,
|
|
12678
|
+
message: "A reusable component was added."
|
|
12679
|
+
});
|
|
12648
12680
|
}
|
|
12649
12681
|
}
|
|
12650
12682
|
}
|
|
12651
|
-
function
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
return
|
|
12683
|
+
function parseOpenApiDocument(value) {
|
|
12684
|
+
if (!isRecord(value) || typeof value.openapi !== "string" || !/^3\.[0-9]+(?:\.[0-9]+)?$/.test(value.openapi) || !isRecord(value.info) || !isRecord(value.paths)) {
|
|
12685
|
+
throw new OpenApiDocumentError;
|
|
12686
|
+
}
|
|
12687
|
+
return { ...value, openapi: value.openapi, info: value.info, paths: value.paths };
|
|
12656
12688
|
}
|
|
12657
|
-
function
|
|
12658
|
-
const
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12689
|
+
function serializeOpenApiJson(document, space = 2) {
|
|
12690
|
+
const parsed = parseOpenApiDocument(document);
|
|
12691
|
+
if (!Number.isInteger(space) || space < 0 || space > 10)
|
|
12692
|
+
throw new OpenApiDocumentError;
|
|
12693
|
+
const serialized = JSON.stringify(parsed, null, space);
|
|
12694
|
+
if (serialized === undefined)
|
|
12695
|
+
throw new OpenApiDocumentError;
|
|
12696
|
+
return `${serialized}
|
|
12697
|
+
`;
|
|
12698
|
+
}
|
|
12699
|
+
async function readOpenApiJson(path) {
|
|
12700
|
+
try {
|
|
12701
|
+
const value = JSON.parse(await readFile10(resolve13(path), "utf8"));
|
|
12702
|
+
return parseOpenApiDocument(value);
|
|
12703
|
+
} catch (error) {
|
|
12704
|
+
if (error instanceof OpenApiDocumentError)
|
|
12705
|
+
throw error;
|
|
12706
|
+
throw new OpenApiDocumentError;
|
|
12666
12707
|
}
|
|
12667
|
-
return result;
|
|
12668
12708
|
}
|
|
12669
|
-
function
|
|
12670
|
-
|
|
12709
|
+
async function writeOpenApiJson(document, outputPath, space = 2) {
|
|
12710
|
+
const path = resolve13(outputPath);
|
|
12711
|
+
const content = serializeOpenApiJson(document, space);
|
|
12712
|
+
await mkdir4(dirname8(path), { recursive: true });
|
|
12713
|
+
try {
|
|
12714
|
+
if (await readFile10(path, "utf8") === content)
|
|
12715
|
+
return { path, written: false };
|
|
12716
|
+
} catch {}
|
|
12717
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
12718
|
+
try {
|
|
12719
|
+
await writeFile5(temporaryPath, content, "utf8");
|
|
12720
|
+
await rename6(temporaryPath, path);
|
|
12721
|
+
} catch (error) {
|
|
12722
|
+
await unlink3(temporaryPath).catch(() => {
|
|
12723
|
+
return;
|
|
12724
|
+
});
|
|
12725
|
+
throw error;
|
|
12726
|
+
}
|
|
12727
|
+
return { path, written: true };
|
|
12671
12728
|
}
|
|
12672
|
-
function
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
const
|
|
12677
|
-
|
|
12678
|
-
|
|
12729
|
+
async function loadGeneratedOpenApiDocument(modulePath) {
|
|
12730
|
+
try {
|
|
12731
|
+
const moduleUrl = pathToFileURL(resolve13(modulePath));
|
|
12732
|
+
moduleUrl.searchParams.set("supacloud-openapi-export", "1");
|
|
12733
|
+
const loaded = await import(moduleUrl.href);
|
|
12734
|
+
if (!isRecord(loaded))
|
|
12735
|
+
throw new OpenApiDocumentError;
|
|
12736
|
+
return parseOpenApiDocument(loaded.OPENAPI_DOCUMENT);
|
|
12737
|
+
} catch (error) {
|
|
12738
|
+
if (error instanceof OpenApiDocumentError)
|
|
12739
|
+
throw error;
|
|
12740
|
+
throw new OpenApiDocumentError;
|
|
12741
|
+
}
|
|
12742
|
+
}
|
|
12743
|
+
async function exportGeneratedOpenApiJson(options) {
|
|
12744
|
+
const document = await loadGeneratedOpenApiDocument(options.modulePath);
|
|
12745
|
+
return writeOpenApiJson(document, options.outputPath, options.space);
|
|
12746
|
+
}
|
|
12747
|
+
function diffOpenApiDocuments(baseValue, currentValue) {
|
|
12748
|
+
const baseDocument = parseOpenApiDocument(baseValue);
|
|
12749
|
+
const currentDocument = parseOpenApiDocument(currentValue);
|
|
12750
|
+
const changes = [];
|
|
12751
|
+
const basePaths = baseDocument.paths;
|
|
12752
|
+
const currentPaths = currentDocument.paths;
|
|
12753
|
+
for (const path of sortedKeys(basePaths)) {
|
|
12754
|
+
const basePathItem = recordValue(basePaths[path]);
|
|
12755
|
+
const currentPathItem = recordValue(currentPaths[path]);
|
|
12756
|
+
if (!basePathItem || !currentPathItem) {
|
|
12679
12757
|
addChange(changes, {
|
|
12680
12758
|
kind: "breaking",
|
|
12681
|
-
code: "
|
|
12682
|
-
path:
|
|
12683
|
-
message: "A
|
|
12759
|
+
code: "path-removed",
|
|
12760
|
+
path: `paths.${path}`,
|
|
12761
|
+
message: "A path from the previous contract was removed."
|
|
12684
12762
|
});
|
|
12685
12763
|
continue;
|
|
12686
12764
|
}
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12765
|
+
for (const method of HTTP_METHODS) {
|
|
12766
|
+
const baseOperation = recordValue(basePathItem[method]);
|
|
12767
|
+
const currentOperation = recordValue(currentPathItem[method]);
|
|
12768
|
+
if (!baseOperation || !currentOperation) {
|
|
12769
|
+
if (baseOperation) {
|
|
12770
|
+
addChange(changes, {
|
|
12771
|
+
kind: "breaking",
|
|
12772
|
+
code: "operation-removed",
|
|
12773
|
+
path: `paths.${path}.${method}`,
|
|
12774
|
+
message: "An operation from the previous contract was removed."
|
|
12775
|
+
});
|
|
12776
|
+
} else if (currentOperation) {
|
|
12777
|
+
addChange(changes, {
|
|
12778
|
+
kind: "non-breaking",
|
|
12779
|
+
code: "operation-added",
|
|
12780
|
+
path: `paths.${path}.${method}`,
|
|
12781
|
+
message: "An operation was added to an existing path."
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
continue;
|
|
12785
|
+
}
|
|
12786
|
+
compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
|
|
12697
12787
|
}
|
|
12698
12788
|
}
|
|
12699
|
-
for (const
|
|
12700
|
-
if (
|
|
12701
|
-
continue;
|
|
12702
|
-
const parameter = currentParameters.get(key);
|
|
12703
|
-
if (!parameter)
|
|
12789
|
+
for (const path of sortedKeys(currentPaths)) {
|
|
12790
|
+
if (basePaths[path] !== undefined)
|
|
12704
12791
|
continue;
|
|
12705
12792
|
addChange(changes, {
|
|
12706
|
-
kind:
|
|
12707
|
-
code:
|
|
12708
|
-
path:
|
|
12709
|
-
message:
|
|
12793
|
+
kind: "non-breaking",
|
|
12794
|
+
code: "path-added",
|
|
12795
|
+
path: `paths.${path}`,
|
|
12796
|
+
message: "A path was added."
|
|
12710
12797
|
});
|
|
12711
12798
|
}
|
|
12799
|
+
compareComponents(baseDocument, currentDocument, changes);
|
|
12800
|
+
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
12801
|
+
return { ok: breaking.length === 0, breaking, changes };
|
|
12712
12802
|
}
|
|
12713
|
-
function
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12803
|
+
function formatOpenApiDiff(result) {
|
|
12804
|
+
if (result.changes.length === 0)
|
|
12805
|
+
return "OpenAPI diff passed: no contract changes.";
|
|
12806
|
+
return [
|
|
12807
|
+
`OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
|
|
12808
|
+
...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
|
|
12809
|
+
].join(`
|
|
12810
|
+
`);
|
|
12811
|
+
}
|
|
12812
|
+
|
|
12813
|
+
// src/migration-assess.ts
|
|
12814
|
+
var STATUS_PRIORITY = {
|
|
12815
|
+
compatible: 0,
|
|
12816
|
+
"needs-review": 1,
|
|
12817
|
+
"not-proven": 2,
|
|
12818
|
+
unsupported: 3,
|
|
12819
|
+
breaking: 4
|
|
12820
|
+
};
|
|
12821
|
+
function isRecord2(value) {
|
|
12822
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12823
|
+
}
|
|
12824
|
+
function arrayLength(value) {
|
|
12825
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
12826
|
+
}
|
|
12827
|
+
async function sha256(path) {
|
|
12828
|
+
return createHash9("sha256").update(await readFile11(path)).digest("hex");
|
|
12829
|
+
}
|
|
12830
|
+
function overallStatus(findings) {
|
|
12831
|
+
return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
|
|
12832
|
+
}
|
|
12833
|
+
async function assessMigration(options) {
|
|
12834
|
+
const projectDir = resolve14(options.projectDir);
|
|
12835
|
+
const compile = {
|
|
12836
|
+
...options.compile,
|
|
12837
|
+
rootDir: resolve14(options.compile.rootDir),
|
|
12838
|
+
outDir: resolve14(options.compile.outDir)
|
|
12839
|
+
};
|
|
12840
|
+
const findings = [];
|
|
12841
|
+
const dependencyProblems = await checkMigrationDependencies(projectDir);
|
|
12842
|
+
if (dependencyProblems.length > 0) {
|
|
12843
|
+
findings.push({
|
|
12844
|
+
code: "migration-dependency-tuple-not-proven",
|
|
12845
|
+
status: "not-proven",
|
|
12846
|
+
message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
|
|
12847
|
+
evidence: dependencyProblems.join("; "),
|
|
12848
|
+
remediation: "Install the exact tested package versions before applying a source migration."
|
|
12849
|
+
});
|
|
12850
|
+
}
|
|
12851
|
+
let compiler = {
|
|
12852
|
+
upToDate: false,
|
|
12853
|
+
diagnostics: [],
|
|
12854
|
+
mismatches: []
|
|
12855
|
+
};
|
|
12856
|
+
try {
|
|
12857
|
+
const checked = await checkProject(compile);
|
|
12858
|
+
compiler = {
|
|
12859
|
+
upToDate: checked.upToDate,
|
|
12860
|
+
diagnostics: checked.diagnostics,
|
|
12861
|
+
mismatches: checked.mismatches
|
|
12862
|
+
};
|
|
12863
|
+
const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
12864
|
+
if (errors.length > 0) {
|
|
12865
|
+
findings.push({
|
|
12866
|
+
code: "compiler-governance-not-proven",
|
|
12867
|
+
status: "not-proven",
|
|
12868
|
+
message: `Compiler governance has ${errors.length} error(s).`,
|
|
12869
|
+
remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
|
|
12730
12870
|
});
|
|
12731
|
-
}
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
code: "
|
|
12735
|
-
|
|
12736
|
-
message: "
|
|
12871
|
+
}
|
|
12872
|
+
if (!checked.upToDate) {
|
|
12873
|
+
findings.push({
|
|
12874
|
+
code: "generated-artifact-drift",
|
|
12875
|
+
status: "needs-review",
|
|
12876
|
+
message: "Generated artifacts do not match the current source and compiler configuration.",
|
|
12877
|
+
evidence: checked.mismatches.join("; "),
|
|
12878
|
+
remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
|
|
12737
12879
|
});
|
|
12738
12880
|
}
|
|
12739
|
-
|
|
12881
|
+
} catch {
|
|
12882
|
+
findings.push({
|
|
12883
|
+
code: "compiler-check-unavailable",
|
|
12884
|
+
status: "not-proven",
|
|
12885
|
+
message: "The local compiler check could not complete.",
|
|
12886
|
+
remediation: "Verify the project source, configuration and installed dependencies, then reassess."
|
|
12887
|
+
});
|
|
12740
12888
|
}
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12889
|
+
const contractsManifestPath = join7(compile.outDir, "contracts.manifest.json");
|
|
12890
|
+
let contractsManifest = {
|
|
12891
|
+
path: contractsManifestPath,
|
|
12892
|
+
present: false
|
|
12893
|
+
};
|
|
12894
|
+
try {
|
|
12895
|
+
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
12896
|
+
if (!isRecord2(parsed) || parsed.version !== 1)
|
|
12897
|
+
throw new Error("invalid manifest");
|
|
12898
|
+
contractsManifest = {
|
|
12899
|
+
path: contractsManifestPath,
|
|
12900
|
+
present: true,
|
|
12901
|
+
sha256: await sha256(contractsManifestPath),
|
|
12902
|
+
version: 1,
|
|
12903
|
+
...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
|
|
12904
|
+
...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
|
|
12905
|
+
...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
|
|
12906
|
+
};
|
|
12907
|
+
} catch {
|
|
12908
|
+
findings.push({
|
|
12909
|
+
code: "contract-manifest-not-proven",
|
|
12910
|
+
status: "not-proven",
|
|
12911
|
+
message: "contracts.manifest.json is missing or invalid.",
|
|
12912
|
+
evidence: contractsManifestPath,
|
|
12913
|
+
remediation: "Generate and review the contract manifest before migration."
|
|
12747
12914
|
});
|
|
12748
12915
|
}
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
const
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12916
|
+
let openApi;
|
|
12917
|
+
if (options.baselineOpenApiPath && options.currentOpenApiPath) {
|
|
12918
|
+
const baselinePath = resolve14(projectDir, options.baselineOpenApiPath);
|
|
12919
|
+
const currentPath = resolve14(projectDir, options.currentOpenApiPath);
|
|
12920
|
+
try {
|
|
12921
|
+
const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
|
|
12922
|
+
openApi = {
|
|
12923
|
+
baselinePath,
|
|
12924
|
+
baselineSha256: await sha256(baselinePath),
|
|
12925
|
+
currentPath,
|
|
12926
|
+
currentSha256: await sha256(currentPath),
|
|
12927
|
+
diff
|
|
12928
|
+
};
|
|
12929
|
+
if (!diff.ok) {
|
|
12930
|
+
findings.push({
|
|
12931
|
+
code: "openapi-breaking-change",
|
|
12932
|
+
status: "breaking",
|
|
12933
|
+
message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
|
|
12934
|
+
remediation: "Review and explicitly approve or redesign each breaking contract change."
|
|
12935
|
+
});
|
|
12936
|
+
} else if (diff.changes.length > 0) {
|
|
12937
|
+
findings.push({
|
|
12938
|
+
code: "openapi-compatible-change-review",
|
|
12939
|
+
status: "needs-review",
|
|
12940
|
+
message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
|
|
12941
|
+
});
|
|
12942
|
+
}
|
|
12943
|
+
} catch {
|
|
12944
|
+
findings.push({
|
|
12945
|
+
code: "openapi-diff-not-proven",
|
|
12946
|
+
status: "not-proven",
|
|
12947
|
+
message: "The OpenAPI baseline or current document could not be loaded.",
|
|
12948
|
+
remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
|
|
12760
12949
|
});
|
|
12761
|
-
continue;
|
|
12762
|
-
}
|
|
12763
|
-
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12764
|
-
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
|
|
12765
12950
|
}
|
|
12951
|
+
} else {
|
|
12952
|
+
findings.push({
|
|
12953
|
+
code: "openapi-baseline-not-proven",
|
|
12954
|
+
status: "not-proven",
|
|
12955
|
+
message: "No complete OpenAPI baseline/current pair was supplied.",
|
|
12956
|
+
remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
|
|
12957
|
+
});
|
|
12766
12958
|
}
|
|
12959
|
+
findings.push({
|
|
12960
|
+
code: "rendering-mode-preserved",
|
|
12961
|
+
status: "compatible",
|
|
12962
|
+
message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
|
|
12963
|
+
evidence: `selected=${options.renderMode ?? "unspecified"}`
|
|
12964
|
+
});
|
|
12965
|
+
const status = overallStatus(findings);
|
|
12966
|
+
return {
|
|
12967
|
+
version: 1,
|
|
12968
|
+
ok: status === "compatible" || status === "needs-review",
|
|
12969
|
+
status,
|
|
12970
|
+
readOnly: true,
|
|
12971
|
+
writesPerformed: false,
|
|
12972
|
+
rendering: {
|
|
12973
|
+
selected: options.renderMode ?? "unspecified",
|
|
12974
|
+
ssrRequired: false,
|
|
12975
|
+
supportedModes: ["browser", "ssr", "edge", "trusted-server"],
|
|
12976
|
+
guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
|
|
12977
|
+
},
|
|
12978
|
+
dependencies: {
|
|
12979
|
+
expected: migrationDependencies(),
|
|
12980
|
+
problems: dependencyProblems
|
|
12981
|
+
},
|
|
12982
|
+
compiler,
|
|
12983
|
+
artifacts: {
|
|
12984
|
+
contractsManifest,
|
|
12985
|
+
...openApi === undefined ? {} : { openApi }
|
|
12986
|
+
},
|
|
12987
|
+
findings
|
|
12988
|
+
};
|
|
12767
12989
|
}
|
|
12768
|
-
function
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12990
|
+
function formatMigrationAssessment(result) {
|
|
12991
|
+
return [
|
|
12992
|
+
`Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
|
|
12993
|
+
`Rendering: ${result.rendering.selected}; SSR required: no.`,
|
|
12994
|
+
...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
|
|
12995
|
+
].join(`
|
|
12996
|
+
`);
|
|
12997
|
+
}
|
|
12998
|
+
// src/inspect.ts
|
|
12999
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
13000
|
+
import { dirname as dirname9, join as join8, relative as relative11, sep as sep10 } from "node:path";
|
|
13001
|
+
function formatGraph(graph) {
|
|
13002
|
+
const lines = [];
|
|
13003
|
+
for (const module of graph.modules) {
|
|
13004
|
+
lines.push(`MODULE ${module.name}`);
|
|
13005
|
+
lines.push(` file: ${module.file}:${module.line}`);
|
|
13006
|
+
lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
|
|
13007
|
+
lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
|
|
13008
|
+
lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
|
|
13009
|
+
lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
|
|
13010
|
+
}
|
|
13011
|
+
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
13012
|
+
return lines.join(`
|
|
13013
|
+
`);
|
|
13014
|
+
}
|
|
13015
|
+
function explainGraph(graph, subject) {
|
|
13016
|
+
const module = graph.modules.find((candidate) => candidate.name === subject);
|
|
13017
|
+
if (module)
|
|
13018
|
+
return explainModule(graph, module);
|
|
13019
|
+
const provider = findProvider(graph, subject);
|
|
13020
|
+
if (provider)
|
|
13021
|
+
return explainProvider(graph, provider.module, provider.provider);
|
|
13022
|
+
if (graph.externalTokens.includes(subject)) {
|
|
13023
|
+
const references = graph.modules.flatMap((candidate) => [
|
|
13024
|
+
...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
|
|
13025
|
+
...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
|
|
13026
|
+
]);
|
|
13027
|
+
return [
|
|
13028
|
+
`EXTERNAL TOKEN ${subject}`,
|
|
13029
|
+
" provided by: platform runtime",
|
|
13030
|
+
` references: ${references.length > 0 ? references.join(", ") : "-"}`
|
|
13031
|
+
].join(`
|
|
13032
|
+
`);
|
|
13033
|
+
}
|
|
13034
|
+
const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
|
|
13035
|
+
throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
|
|
13036
|
+
}
|
|
13037
|
+
function createContextPack(graph, subject) {
|
|
13038
|
+
const subjectModule = graph.modules.find((module) => module.name === subject);
|
|
13039
|
+
if (!subjectModule) {
|
|
13040
|
+
throw new Error(`No module named "${subject}". Context packs require a module name.`);
|
|
13041
|
+
}
|
|
13042
|
+
const byName = new Map(graph.modules.map((module) => [module.name, module]));
|
|
13043
|
+
const selected = new Set([subjectModule.name]);
|
|
13044
|
+
const reverseDependents = new Map;
|
|
13045
|
+
for (const mod of graph.modules) {
|
|
13046
|
+
for (const imp of mod.imports) {
|
|
13047
|
+
const list = reverseDependents.get(imp);
|
|
13048
|
+
if (list)
|
|
13049
|
+
list.push(mod.name);
|
|
13050
|
+
else
|
|
13051
|
+
reverseDependents.set(imp, [mod.name]);
|
|
12782
13052
|
}
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
code: "response-media-type-removed",
|
|
12792
|
-
path: `${path}.responses.${status}.content.${mediaType}`,
|
|
12793
|
-
message: "A response media type from the previous contract was removed."
|
|
12794
|
-
});
|
|
13053
|
+
}
|
|
13054
|
+
for (const direction of ["imports", "dependents"]) {
|
|
13055
|
+
const visited = new Set([subjectModule.name]);
|
|
13056
|
+
const queue = [subjectModule.name];
|
|
13057
|
+
let head = 0;
|
|
13058
|
+
while (head < queue.length) {
|
|
13059
|
+
const current = queue[head++];
|
|
13060
|
+
if (!current)
|
|
12795
13061
|
continue;
|
|
12796
|
-
|
|
12797
|
-
if (
|
|
12798
|
-
|
|
13062
|
+
const module = byName.get(current);
|
|
13063
|
+
if (!module)
|
|
13064
|
+
continue;
|
|
13065
|
+
const neighbors = direction === "imports" ? module.imports : reverseDependents.get(module.name) ?? [];
|
|
13066
|
+
for (const neighbor of neighbors) {
|
|
13067
|
+
if (!visited.has(neighbor) && byName.has(neighbor)) {
|
|
13068
|
+
visited.add(neighbor);
|
|
13069
|
+
selected.add(neighbor);
|
|
13070
|
+
queue.push(neighbor);
|
|
13071
|
+
}
|
|
12799
13072
|
}
|
|
12800
13073
|
}
|
|
12801
13074
|
}
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
13075
|
+
const modules = graph.modules.filter((module) => selected.has(module.name));
|
|
13076
|
+
const queryDocuments = graph.graphql?.documents.filter((file) => modules.some((module) => {
|
|
13077
|
+
const path = relative11(dirname9(module.file), file);
|
|
13078
|
+
return path !== ".." && !path.startsWith(`..${sep10}`);
|
|
13079
|
+
})) ?? [];
|
|
13080
|
+
const graphql = graph.graphql ? {
|
|
13081
|
+
...graph.graphql,
|
|
13082
|
+
documents: queryDocuments,
|
|
13083
|
+
operations: graph.graphql.operations.filter((operation) => queryDocuments.includes(operation.file))
|
|
13084
|
+
} : undefined;
|
|
13085
|
+
const files = [...new Set(modules.flatMap((module) => [
|
|
13086
|
+
module.file,
|
|
13087
|
+
...module.providers.map((provider) => provider.file),
|
|
13088
|
+
...module.controllers.map((controller) => controller.file),
|
|
13089
|
+
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
13090
|
+
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
13091
|
+
const referencedTokens = new Set;
|
|
13092
|
+
for (const module of modules) {
|
|
13093
|
+
for (const provider of module.providers) {
|
|
13094
|
+
for (const token of provider.deps)
|
|
13095
|
+
referencedTokens.add(token);
|
|
13096
|
+
}
|
|
13097
|
+
for (const controller of module.controllers) {
|
|
13098
|
+
for (const token of controller.deps)
|
|
13099
|
+
referencedTokens.add(token);
|
|
13100
|
+
}
|
|
12811
13101
|
}
|
|
13102
|
+
return {
|
|
13103
|
+
version: 1,
|
|
13104
|
+
subject: subjectModule.name,
|
|
13105
|
+
modules,
|
|
13106
|
+
files,
|
|
13107
|
+
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
13108
|
+
executionPlans: createExecutionPlans({ ...graph, modules }),
|
|
13109
|
+
routeContracts: inspectRouteContracts({ ...graph, modules }),
|
|
13110
|
+
...graphql ? { graphql } : {},
|
|
13111
|
+
diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
|
|
13112
|
+
relatedModules: {
|
|
13113
|
+
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
13114
|
+
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
13115
|
+
}
|
|
13116
|
+
};
|
|
12812
13117
|
}
|
|
12813
|
-
function
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
13118
|
+
function allAspects(module) {
|
|
13119
|
+
return [
|
|
13120
|
+
...module.aspects ?? [],
|
|
13121
|
+
...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
|
|
13122
|
+
...module.commands.flatMap((command) => command.aspects ?? []),
|
|
13123
|
+
...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
|
|
13124
|
+
];
|
|
13125
|
+
}
|
|
13126
|
+
function createExecutionPlans(graph) {
|
|
13127
|
+
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
13128
|
+
return graph.modules.flatMap((module) => [
|
|
13129
|
+
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
13130
|
+
const command = module.commands.find((item) => item.className === route.command);
|
|
13131
|
+
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
13132
|
+
return {
|
|
13133
|
+
module: module.name,
|
|
13134
|
+
kind: "route",
|
|
13135
|
+
name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
|
|
13136
|
+
...command ? { command: command.name } : {},
|
|
13137
|
+
stages: [
|
|
13138
|
+
...aspects(`module:${module.name}`, module.aspects),
|
|
13139
|
+
...aspects("route", route.aspects),
|
|
13140
|
+
...aspects("command", command?.aspects),
|
|
13141
|
+
...command ? [
|
|
13142
|
+
"commandExecutor",
|
|
13143
|
+
"authorize",
|
|
13144
|
+
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
13145
|
+
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
13146
|
+
...command.transaction === "required" ? ["transaction"] : []
|
|
13147
|
+
]
|
|
13148
|
+
] : [],
|
|
13149
|
+
"handler",
|
|
13150
|
+
...command?.audit && !command.rpc ? ["audit"] : []
|
|
13151
|
+
]
|
|
13152
|
+
};
|
|
13153
|
+
})),
|
|
13154
|
+
...module.commands.map((command) => ({
|
|
13155
|
+
module: module.name,
|
|
13156
|
+
kind: "command",
|
|
13157
|
+
name: command.name,
|
|
13158
|
+
command: command.name,
|
|
13159
|
+
stages: [
|
|
13160
|
+
"authorize",
|
|
13161
|
+
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
13162
|
+
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
13163
|
+
...command.transaction === "required" ? ["transaction"] : []
|
|
13164
|
+
],
|
|
13165
|
+
...aspects(`module:${module.name}`, module.aspects),
|
|
13166
|
+
...aspects("command", command.aspects),
|
|
13167
|
+
"handler",
|
|
13168
|
+
...command.audit && !command.rpc ? ["audit"] : []
|
|
13169
|
+
]
|
|
13170
|
+
})),
|
|
13171
|
+
...(module.jobs ?? []).map((job) => ({
|
|
13172
|
+
module: module.name,
|
|
13173
|
+
kind: "job",
|
|
13174
|
+
name: job.name,
|
|
13175
|
+
stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
|
|
13176
|
+
}))
|
|
13177
|
+
]);
|
|
12827
13178
|
}
|
|
12828
|
-
function
|
|
12829
|
-
const
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
code: "component-added",
|
|
12850
|
-
path: `components.${group}.${name}`,
|
|
12851
|
-
message: "A reusable component was added."
|
|
12852
|
-
});
|
|
13179
|
+
function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
|
|
13180
|
+
const checks = [
|
|
13181
|
+
{
|
|
13182
|
+
name: "project-root",
|
|
13183
|
+
ok: existsSync4(rootDir),
|
|
13184
|
+
detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
|
|
13185
|
+
},
|
|
13186
|
+
{
|
|
13187
|
+
name: "tsconfig",
|
|
13188
|
+
ok: existsSync4(join8(rootDir, "tsconfig.json")),
|
|
13189
|
+
detail: existsSync4(join8(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
13190
|
+
},
|
|
13191
|
+
{
|
|
13192
|
+
name: "modules",
|
|
13193
|
+
ok: graph.modules.length > 0,
|
|
13194
|
+
detail: `${graph.modules.length} module(s) discovered`
|
|
13195
|
+
},
|
|
13196
|
+
{
|
|
13197
|
+
name: "generated-artifacts",
|
|
13198
|
+
ok: upToDate,
|
|
13199
|
+
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
12853
13200
|
}
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
13201
|
+
];
|
|
13202
|
+
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
13203
|
+
return {
|
|
13204
|
+
checks,
|
|
13205
|
+
diagnostics: allDiagnostics,
|
|
13206
|
+
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
13207
|
+
};
|
|
12861
13208
|
}
|
|
12862
|
-
function
|
|
12863
|
-
const
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
13209
|
+
function explainModule(graph, module) {
|
|
13210
|
+
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
13211
|
+
return [
|
|
13212
|
+
`MODULE ${module.name}`,
|
|
13213
|
+
` file: ${module.file}:${module.line}`,
|
|
13214
|
+
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
13215
|
+
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
13216
|
+
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
13217
|
+
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
13218
|
+
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
|
|
13219
|
+
...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
|
|
13220
|
+
].join(`
|
|
13221
|
+
`);
|
|
12871
13222
|
}
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
}
|
|
13223
|
+
function explainProvider(graph, module, provider) {
|
|
13224
|
+
const dependents = graph.modules.flatMap((candidate) => [
|
|
13225
|
+
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
13226
|
+
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
13227
|
+
]);
|
|
13228
|
+
return [
|
|
13229
|
+
`PROVIDER ${provider.token}`,
|
|
13230
|
+
` module: ${module.name}`,
|
|
13231
|
+
` file: ${provider.file}:${provider.line}`,
|
|
13232
|
+
` kind: ${provider.kind}`,
|
|
13233
|
+
` scope: ${provider.scope}`,
|
|
13234
|
+
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
13235
|
+
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
13236
|
+
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
13237
|
+
].join(`
|
|
13238
|
+
`);
|
|
12881
13239
|
}
|
|
12882
|
-
|
|
12883
|
-
const
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
if (await readFile10(path, "utf8") === content)
|
|
12888
|
-
return { path, written: false };
|
|
12889
|
-
} catch {}
|
|
12890
|
-
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
12891
|
-
try {
|
|
12892
|
-
await writeFile5(temporaryPath, content, "utf8");
|
|
12893
|
-
await rename6(temporaryPath, path);
|
|
12894
|
-
} catch (error) {
|
|
12895
|
-
await unlink3(temporaryPath).catch(() => {
|
|
12896
|
-
return;
|
|
12897
|
-
});
|
|
12898
|
-
throw error;
|
|
13240
|
+
function findProvider(graph, subject) {
|
|
13241
|
+
for (const module of graph.modules) {
|
|
13242
|
+
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
13243
|
+
if (provider)
|
|
13244
|
+
return { module, provider };
|
|
12899
13245
|
}
|
|
12900
|
-
return
|
|
13246
|
+
return;
|
|
12901
13247
|
}
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
if (error instanceof OpenApiDocumentError)
|
|
12912
|
-
throw error;
|
|
12913
|
-
throw new OpenApiDocumentError;
|
|
13248
|
+
function exportGraphMermaid(graph) {
|
|
13249
|
+
const lines = ["graph TD"];
|
|
13250
|
+
for (const mod of graph.modules) {
|
|
13251
|
+
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
13252
|
+
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
13253
|
+
for (const imp of mod.imports) {
|
|
13254
|
+
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
13255
|
+
lines.push(` ${safeId} --> ${safeImp}`);
|
|
13256
|
+
}
|
|
12914
13257
|
}
|
|
13258
|
+
return lines.join(`
|
|
13259
|
+
`);
|
|
12915
13260
|
}
|
|
12916
|
-
|
|
12917
|
-
const
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
const
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
for (const path of sortedKeys(basePaths)) {
|
|
12927
|
-
const basePathItem = recordValue(basePaths[path]);
|
|
12928
|
-
const currentPathItem = recordValue(currentPaths[path]);
|
|
12929
|
-
if (!basePathItem || !currentPathItem) {
|
|
12930
|
-
addChange(changes, {
|
|
12931
|
-
kind: "breaking",
|
|
12932
|
-
code: "path-removed",
|
|
12933
|
-
path: `paths.${path}`,
|
|
12934
|
-
message: "A path from the previous contract was removed."
|
|
12935
|
-
});
|
|
12936
|
-
continue;
|
|
12937
|
-
}
|
|
12938
|
-
for (const method of HTTP_METHODS) {
|
|
12939
|
-
const baseOperation = recordValue(basePathItem[method]);
|
|
12940
|
-
const currentOperation = recordValue(currentPathItem[method]);
|
|
12941
|
-
if (!baseOperation || !currentOperation) {
|
|
12942
|
-
if (baseOperation) {
|
|
12943
|
-
addChange(changes, {
|
|
12944
|
-
kind: "breaking",
|
|
12945
|
-
code: "operation-removed",
|
|
12946
|
-
path: `paths.${path}.${method}`,
|
|
12947
|
-
message: "An operation from the previous contract was removed."
|
|
12948
|
-
});
|
|
12949
|
-
} else if (currentOperation) {
|
|
12950
|
-
addChange(changes, {
|
|
12951
|
-
kind: "non-breaking",
|
|
12952
|
-
code: "operation-added",
|
|
12953
|
-
path: `paths.${path}.${method}`,
|
|
12954
|
-
message: "An operation was added to an existing path."
|
|
12955
|
-
});
|
|
12956
|
-
}
|
|
12957
|
-
continue;
|
|
12958
|
-
}
|
|
12959
|
-
compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
|
|
13261
|
+
function exportGraphDot(graph) {
|
|
13262
|
+
const lines = [
|
|
13263
|
+
"digraph ApplicationGraph {",
|
|
13264
|
+
" rankdir=LR;",
|
|
13265
|
+
' node [shape=box, fontname="Helvetica"];'
|
|
13266
|
+
];
|
|
13267
|
+
for (const mod of graph.modules) {
|
|
13268
|
+
lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
|
|
13269
|
+
for (const imp of mod.imports) {
|
|
13270
|
+
lines.push(` "${mod.name}" -> "${imp}";`);
|
|
12960
13271
|
}
|
|
12961
13272
|
}
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
continue;
|
|
12965
|
-
addChange(changes, {
|
|
12966
|
-
kind: "non-breaking",
|
|
12967
|
-
code: "path-added",
|
|
12968
|
-
path: `paths.${path}`,
|
|
12969
|
-
message: "A path was added."
|
|
12970
|
-
});
|
|
12971
|
-
}
|
|
12972
|
-
compareComponents(baseDocument, currentDocument, changes);
|
|
12973
|
-
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
12974
|
-
return { ok: breaking.length === 0, breaking, changes };
|
|
12975
|
-
}
|
|
12976
|
-
function formatOpenApiDiff(result) {
|
|
12977
|
-
if (result.changes.length === 0)
|
|
12978
|
-
return "OpenAPI diff passed: no contract changes.";
|
|
12979
|
-
return [
|
|
12980
|
-
`OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
|
|
12981
|
-
...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
|
|
12982
|
-
].join(`
|
|
13273
|
+
lines.push("}");
|
|
13274
|
+
return lines.join(`
|
|
12983
13275
|
`);
|
|
12984
13276
|
}
|
|
13277
|
+
|
|
13278
|
+
// src/index.ts
|
|
13279
|
+
init_generate();
|
|
13280
|
+
init_contract_manifest();
|
|
13281
|
+
|
|
12985
13282
|
// src/config.ts
|
|
12986
13283
|
init_graphql_options();
|
|
12987
13284
|
import { existsSync as existsSync5 } from "node:fs";
|
|
12988
|
-
import { join as
|
|
13285
|
+
import { join as join9, resolve as resolve15 } from "node:path";
|
|
12989
13286
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
12990
13287
|
var DEFAULT_SUPACLOUD_CONFIG = {
|
|
12991
13288
|
graphql: false,
|
|
@@ -13078,8 +13375,8 @@ function validateGovernanceConfig(config) {
|
|
|
13078
13375
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
13079
13376
|
const resolved = defineSupacloudConfig(config);
|
|
13080
13377
|
return {
|
|
13081
|
-
rootDir:
|
|
13082
|
-
outDir:
|
|
13378
|
+
rootDir: resolve15(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
|
|
13379
|
+
outDir: resolve15(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
13083
13380
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
13084
13381
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
13085
13382
|
requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
|
|
@@ -13097,16 +13394,16 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
13097
13394
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
13098
13395
|
graphql: resolved.graphql ? {
|
|
13099
13396
|
...resolved.graphql,
|
|
13100
|
-
schema:
|
|
13397
|
+
schema: resolve15(cwd, resolved.graphql.schema)
|
|
13101
13398
|
} : undefined
|
|
13102
13399
|
};
|
|
13103
13400
|
}
|
|
13104
13401
|
async function loadSupacloudConfig(cwd = process.cwd()) {
|
|
13105
13402
|
const candidates = [
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13403
|
+
join9(cwd, "supacloud.config.ts"),
|
|
13404
|
+
join9(cwd, "supacloud.config.mts"),
|
|
13405
|
+
join9(cwd, "supacloud.config.js"),
|
|
13406
|
+
join9(cwd, "supacloud.config.mjs")
|
|
13110
13407
|
];
|
|
13111
13408
|
const configPath = candidates.find((candidate) => existsSync5(candidate));
|
|
13112
13409
|
if (!configPath)
|
|
@@ -13143,6 +13440,8 @@ export {
|
|
|
13143
13440
|
TraitCompiler,
|
|
13144
13441
|
analyzeProject,
|
|
13145
13442
|
applyDiagnosticFix,
|
|
13443
|
+
assessMigration,
|
|
13444
|
+
buildContractManifest,
|
|
13146
13445
|
buildDeliveryProject,
|
|
13147
13446
|
camelName,
|
|
13148
13447
|
checkProject,
|
|
@@ -13164,6 +13463,7 @@ export {
|
|
|
13164
13463
|
exportGraphMermaid,
|
|
13165
13464
|
formatDeliveryPlan,
|
|
13166
13465
|
formatGraph,
|
|
13466
|
+
formatMigrationAssessment,
|
|
13167
13467
|
formatOpenApiDiff,
|
|
13168
13468
|
generateApplication,
|
|
13169
13469
|
generateDatabaseContracts,
|