@supacloud/compiler 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +290 -44
- package/dist/contract-manifest.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +945 -737
- package/dist/migration-assess.d.ts +62 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -125,6 +125,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
125
125
|
...command?.permission === undefined ? {} : { permission: command.permission },
|
|
126
126
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
127
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
|
+
},
|
|
128
131
|
...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
|
|
129
132
|
});
|
|
130
133
|
}
|
|
@@ -2696,9 +2699,9 @@ var init_graphql = __esm(() => {
|
|
|
2696
2699
|
});
|
|
2697
2700
|
|
|
2698
2701
|
// src/graphql-schema.ts
|
|
2699
|
-
import { mkdir as mkdir5, readFile as
|
|
2700
|
-
import { createHash as
|
|
2701
|
-
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";
|
|
2702
2705
|
async function pullGraphqlSchema(options) {
|
|
2703
2706
|
assertGraphqlOptions({ schema: options.output });
|
|
2704
2707
|
const endpoint = new URL(options.url);
|
|
@@ -2738,7 +2741,7 @@ async function pullGraphqlSchema(options) {
|
|
|
2738
2741
|
throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
|
|
2739
2742
|
}
|
|
2740
2743
|
const schema = lexicographicSortSchema(buildClientSchema(data));
|
|
2741
|
-
const path =
|
|
2744
|
+
const path = resolve16(options.output);
|
|
2742
2745
|
const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
|
|
2743
2746
|
` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
|
|
2744
2747
|
# Database First: change database declarations, apply migrations, then re-export for the intended role.
|
|
@@ -2746,13 +2749,13 @@ async function pullGraphqlSchema(options) {
|
|
|
2746
2749
|
`;
|
|
2747
2750
|
let previous;
|
|
2748
2751
|
try {
|
|
2749
|
-
previous = await
|
|
2752
|
+
previous = await readFile12(path, "utf8");
|
|
2750
2753
|
} catch (error) {
|
|
2751
2754
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2752
2755
|
throw error;
|
|
2753
2756
|
}
|
|
2754
2757
|
const upToDate = previous === content;
|
|
2755
|
-
const schemaHash =
|
|
2758
|
+
const schemaHash = createHash10("sha256").update(content).digest("hex");
|
|
2756
2759
|
if (options.check)
|
|
2757
2760
|
return { path, schemaHash, upToDate, written: false };
|
|
2758
2761
|
await mkdir5(dirname10(path), { recursive: true });
|
|
@@ -2765,12 +2768,12 @@ var init_graphql_schema = __esm(() => {
|
|
|
2765
2768
|
});
|
|
2766
2769
|
|
|
2767
2770
|
// src/database-contracts.ts
|
|
2768
|
-
import { createHash as
|
|
2769
|
-
import { mkdir as mkdir6, readFile as
|
|
2770
|
-
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";
|
|
2771
2774
|
import * as ts13 from "@typescript/typescript6";
|
|
2772
2775
|
function hash2(value) {
|
|
2773
|
-
return
|
|
2776
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
2774
2777
|
}
|
|
2775
2778
|
function importPath(out, path) {
|
|
2776
2779
|
const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
|
|
@@ -2795,19 +2798,19 @@ function parseDatabaseContractsOptions(value, directory) {
|
|
|
2795
2798
|
throw new TypeError("migrations must be an ordered list of SQL files");
|
|
2796
2799
|
}
|
|
2797
2800
|
return {
|
|
2798
|
-
rootDir:
|
|
2799
|
-
outDir:
|
|
2800
|
-
postgrestTypes:
|
|
2801
|
-
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")),
|
|
2802
2805
|
role: field("role"),
|
|
2803
|
-
graphql: { ...graphql, schema:
|
|
2804
|
-
migrations: migrations.map((file) =>
|
|
2806
|
+
graphql: { ...graphql, schema: resolve17(directory, graphql.schema) },
|
|
2807
|
+
migrations: migrations.map((file) => resolve17(directory, file))
|
|
2805
2808
|
};
|
|
2806
2809
|
}
|
|
2807
2810
|
async function generateDatabaseContracts(options, check = false) {
|
|
2808
|
-
const rootDir =
|
|
2809
|
-
const postgrestTypes =
|
|
2810
|
-
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");
|
|
2811
2814
|
const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
|
|
2812
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));
|
|
2813
2816
|
if (!database)
|
|
@@ -2834,8 +2837,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2834
2837
|
}
|
|
2835
2838
|
const inputs = {};
|
|
2836
2839
|
const addInput = async (path) => {
|
|
2837
|
-
const absolute =
|
|
2838
|
-
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await
|
|
2840
|
+
const absolute = resolve17(rootDir, path);
|
|
2841
|
+
inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile13(absolute, "utf8"));
|
|
2839
2842
|
};
|
|
2840
2843
|
await addInput(postgrestTypes);
|
|
2841
2844
|
await addInput(drizzleSchema);
|
|
@@ -2851,8 +2854,8 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2851
2854
|
if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
|
|
2852
2855
|
await addInput(source.fileName);
|
|
2853
2856
|
}
|
|
2854
|
-
await addInput(
|
|
2855
|
-
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) {
|
|
2856
2859
|
throw new Error("Duplicate migration in database contracts configuration");
|
|
2857
2860
|
}
|
|
2858
2861
|
for (const path of options.migrations)
|
|
@@ -2873,17 +2876,17 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2873
2876
|
version: 1,
|
|
2874
2877
|
role: options.role,
|
|
2875
2878
|
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
|
|
2876
|
-
migrationOrder: options.migrations.map((path) => relative12(rootDir,
|
|
2879
|
+
migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve17(rootDir, path)).replaceAll("\\", "/")),
|
|
2877
2880
|
outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
|
|
2878
2881
|
};
|
|
2879
2882
|
files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
|
|
2880
2883
|
`;
|
|
2881
2884
|
const mismatches = [];
|
|
2882
2885
|
for (const [file, content] of Object.entries(files)) {
|
|
2883
|
-
const path =
|
|
2886
|
+
const path = resolve17(outDir, file);
|
|
2884
2887
|
let current;
|
|
2885
2888
|
try {
|
|
2886
|
-
current = await
|
|
2889
|
+
current = await readFile13(path, "utf8");
|
|
2887
2890
|
} catch (error) {
|
|
2888
2891
|
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
2889
2892
|
throw error;
|
|
@@ -2894,13 +2897,13 @@ async function generateDatabaseContracts(options, check = false) {
|
|
|
2894
2897
|
if (!check) {
|
|
2895
2898
|
await mkdir6(outDir, { recursive: true });
|
|
2896
2899
|
for (const [file, content] of Object.entries(files))
|
|
2897
|
-
await writeFileIfChanged(
|
|
2900
|
+
await writeFileIfChanged(resolve17(outDir, file), content);
|
|
2898
2901
|
}
|
|
2899
2902
|
return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
|
|
2900
2903
|
}
|
|
2901
2904
|
async function runDatabaseContractsFile(path, check = false) {
|
|
2902
|
-
const absolute =
|
|
2903
|
-
const value = JSON.parse(await
|
|
2905
|
+
const absolute = resolve17(path);
|
|
2906
|
+
const value = JSON.parse(await readFile13(absolute, "utf8"));
|
|
2904
2907
|
return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname11(absolute)), check);
|
|
2905
2908
|
}
|
|
2906
2909
|
var init_database_contracts = __esm(() => {
|
|
@@ -11585,6 +11588,8 @@ function optionsKeyOf(options) {
|
|
|
11585
11588
|
requireRouteContracts: options.requireRouteContracts,
|
|
11586
11589
|
detectOrphanModules: options.detectOrphanModules,
|
|
11587
11590
|
generateClient: options.generateClient,
|
|
11591
|
+
generateOpenApi: options.generateOpenApi,
|
|
11592
|
+
openApi: options.openApi,
|
|
11588
11593
|
generatePermissions: options.generatePermissions,
|
|
11589
11594
|
typeSafety: options.typeSafety,
|
|
11590
11595
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
@@ -11719,6 +11724,16 @@ function findAffectedModules(previous, current, changedFiles) {
|
|
|
11719
11724
|
|
|
11720
11725
|
// src/watch.ts
|
|
11721
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
|
+
}
|
|
11722
11737
|
function watchProject(options) {
|
|
11723
11738
|
const rootDir = resolve10(options.rootDir);
|
|
11724
11739
|
const outDir = resolve10(options.outDir);
|
|
@@ -11811,7 +11826,7 @@ function watchProject(options) {
|
|
|
11811
11826
|
const relativePath = relative9(outDir, changedPath);
|
|
11812
11827
|
if (!relativePath.startsWith("..") && relativePath !== "")
|
|
11813
11828
|
return;
|
|
11814
|
-
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)) {
|
|
11815
11830
|
schedule(relative9(rootDir, changedPath));
|
|
11816
11831
|
}
|
|
11817
11832
|
});
|
|
@@ -12285,798 +12300,989 @@ async function migrateProject(options) {
|
|
|
12285
12300
|
issues
|
|
12286
12301
|
};
|
|
12287
12302
|
}
|
|
12288
|
-
// src/
|
|
12289
|
-
import {
|
|
12290
|
-
import {
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
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";
|
|
12300
12318
|
}
|
|
12301
|
-
lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
|
|
12302
|
-
return lines.join(`
|
|
12303
|
-
`);
|
|
12304
12319
|
}
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
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);
|
|
12326
12340
|
}
|
|
12327
|
-
function
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
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
|
+
});
|
|
12331
12409
|
}
|
|
12332
|
-
const
|
|
12333
|
-
const
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
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
|
+
}
|
|
12342
12423
|
}
|
|
12343
12424
|
}
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
if (!visited.has(neighbor) && byName.has(neighbor)) {
|
|
12358
|
-
visited.add(neighbor);
|
|
12359
|
-
selected.add(neighbor);
|
|
12360
|
-
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
|
+
});
|
|
12361
12438
|
}
|
|
12439
|
+
continue;
|
|
12362
12440
|
}
|
|
12441
|
+
compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
|
|
12363
12442
|
}
|
|
12364
12443
|
}
|
|
12365
|
-
const
|
|
12366
|
-
const
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
...module.providers.map((provider) => provider.file),
|
|
12378
|
-
...module.controllers.map((controller) => controller.file),
|
|
12379
|
-
...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
|
|
12380
|
-
]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
|
|
12381
|
-
const referencedTokens = new Set;
|
|
12382
|
-
for (const module of modules) {
|
|
12383
|
-
for (const provider of module.providers) {
|
|
12384
|
-
for (const token of provider.deps)
|
|
12385
|
-
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
|
+
}
|
|
12386
12456
|
}
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
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
|
+
});
|
|
12464
|
+
}
|
|
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
|
+
}
|
|
12390
12475
|
}
|
|
12391
12476
|
}
|
|
12392
|
-
return {
|
|
12393
|
-
version: 1,
|
|
12394
|
-
subject: subjectModule.name,
|
|
12395
|
-
modules,
|
|
12396
|
-
files,
|
|
12397
|
-
externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
|
|
12398
|
-
executionPlans: createExecutionPlans({ ...graph, modules }),
|
|
12399
|
-
routeContracts: inspectRouteContracts({ ...graph, modules }),
|
|
12400
|
-
...graphql ? { graphql } : {},
|
|
12401
|
-
diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
|
|
12402
|
-
relatedModules: {
|
|
12403
|
-
imports: subjectModule.imports.filter((name) => selected.has(name)),
|
|
12404
|
-
importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
|
|
12405
|
-
}
|
|
12406
|
-
};
|
|
12407
12477
|
}
|
|
12408
|
-
function
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
|
|
12414
|
-
];
|
|
12415
|
-
}
|
|
12416
|
-
function createExecutionPlans(graph) {
|
|
12417
|
-
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
12418
|
-
return graph.modules.flatMap((module) => [
|
|
12419
|
-
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
12420
|
-
const command = module.commands.find((item) => item.className === route.command);
|
|
12421
|
-
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
12422
|
-
return {
|
|
12423
|
-
module: module.name,
|
|
12424
|
-
kind: "route",
|
|
12425
|
-
name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
|
|
12426
|
-
...command ? { command: command.name } : {},
|
|
12427
|
-
stages: [
|
|
12428
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12429
|
-
...aspects("route", route.aspects),
|
|
12430
|
-
...aspects("command", command?.aspects),
|
|
12431
|
-
...command ? [
|
|
12432
|
-
"commandExecutor",
|
|
12433
|
-
"authorize",
|
|
12434
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12435
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12436
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12437
|
-
]
|
|
12438
|
-
] : [],
|
|
12439
|
-
"handler",
|
|
12440
|
-
...command?.audit && !command.rpc ? ["audit"] : []
|
|
12441
|
-
]
|
|
12442
|
-
};
|
|
12443
|
-
})),
|
|
12444
|
-
...module.commands.map((command) => ({
|
|
12445
|
-
module: module.name,
|
|
12446
|
-
kind: "command",
|
|
12447
|
-
name: command.name,
|
|
12448
|
-
command: command.name,
|
|
12449
|
-
stages: [
|
|
12450
|
-
"authorize",
|
|
12451
|
-
...command.rpc ? [`rpc:${command.rpc}`] : [
|
|
12452
|
-
...command.idempotency === "required" ? ["idempotency"] : [],
|
|
12453
|
-
...command.transaction === "required" ? ["transaction"] : []
|
|
12454
|
-
],
|
|
12455
|
-
...aspects(`module:${module.name}`, module.aspects),
|
|
12456
|
-
...aspects("command", command.aspects),
|
|
12457
|
-
"handler",
|
|
12458
|
-
...command.audit && !command.rpc ? ["audit"] : []
|
|
12459
|
-
]
|
|
12460
|
-
})),
|
|
12461
|
-
...(module.jobs ?? []).map((job) => ({
|
|
12462
|
-
module: module.name,
|
|
12463
|
-
kind: "job",
|
|
12464
|
-
name: job.name,
|
|
12465
|
-
stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
|
|
12466
|
-
}))
|
|
12467
|
-
]);
|
|
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;
|
|
12468
12483
|
}
|
|
12469
|
-
function
|
|
12470
|
-
const
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
name: "tsconfig",
|
|
12478
|
-
ok: existsSync4(join7(rootDir, "tsconfig.json")),
|
|
12479
|
-
detail: existsSync4(join7(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
|
|
12480
|
-
},
|
|
12481
|
-
{
|
|
12482
|
-
name: "modules",
|
|
12483
|
-
ok: graph.modules.length > 0,
|
|
12484
|
-
detail: `${graph.modules.length} module(s) discovered`
|
|
12485
|
-
},
|
|
12486
|
-
{
|
|
12487
|
-
name: "generated-artifacts",
|
|
12488
|
-
ok: upToDate,
|
|
12489
|
-
detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
|
|
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);
|
|
12490
12492
|
}
|
|
12491
|
-
];
|
|
12492
|
-
const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
|
|
12493
|
-
return {
|
|
12494
|
-
checks,
|
|
12495
|
-
diagnostics: allDiagnostics,
|
|
12496
|
-
errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
|
|
12497
|
-
};
|
|
12498
|
-
}
|
|
12499
|
-
function explainModule(graph, module) {
|
|
12500
|
-
const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
|
|
12501
|
-
return [
|
|
12502
|
-
`MODULE ${module.name}`,
|
|
12503
|
-
` file: ${module.file}:${module.line}`,
|
|
12504
|
-
` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
|
|
12505
|
-
` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
|
|
12506
|
-
` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
|
|
12507
|
-
` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
|
|
12508
|
-
` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
|
|
12509
|
-
...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
|
|
12510
|
-
].join(`
|
|
12511
|
-
`);
|
|
12512
|
-
}
|
|
12513
|
-
function explainProvider(graph, module, provider) {
|
|
12514
|
-
const dependents = graph.modules.flatMap((candidate) => [
|
|
12515
|
-
...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
|
|
12516
|
-
...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
|
|
12517
|
-
]);
|
|
12518
|
-
return [
|
|
12519
|
-
`PROVIDER ${provider.token}`,
|
|
12520
|
-
` module: ${module.name}`,
|
|
12521
|
-
` file: ${provider.file}:${provider.line}`,
|
|
12522
|
-
` kind: ${provider.kind}`,
|
|
12523
|
-
` scope: ${provider.scope}`,
|
|
12524
|
-
` exported: ${provider.exported ? "yes" : "no"}`,
|
|
12525
|
-
` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
|
|
12526
|
-
` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
|
|
12527
|
-
].join(`
|
|
12528
|
-
`);
|
|
12529
|
-
}
|
|
12530
|
-
function findProvider(graph, subject) {
|
|
12531
|
-
for (const module of graph.modules) {
|
|
12532
|
-
const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
|
|
12533
|
-
if (provider)
|
|
12534
|
-
return { module, provider };
|
|
12535
12493
|
}
|
|
12536
|
-
return;
|
|
12494
|
+
return result;
|
|
12537
12495
|
}
|
|
12538
|
-
function
|
|
12539
|
-
|
|
12540
|
-
for (const mod of graph.modules) {
|
|
12541
|
-
const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12542
|
-
lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
|
|
12543
|
-
for (const imp of mod.imports) {
|
|
12544
|
-
const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12545
|
-
lines.push(` ${safeId} --> ${safeImp}`);
|
|
12546
|
-
}
|
|
12547
|
-
}
|
|
12548
|
-
return lines.join(`
|
|
12549
|
-
`);
|
|
12496
|
+
function parameterSchema(value) {
|
|
12497
|
+
return value.schema;
|
|
12550
12498
|
}
|
|
12551
|
-
function
|
|
12552
|
-
const
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
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);
|
|
12561
12524
|
}
|
|
12562
12525
|
}
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
import { pathToFileURL } from "node:url";
|
|
12576
|
-
|
|
12577
|
-
class OpenApiDocumentError extends Error {
|
|
12578
|
-
code = "OPENAPI_DOCUMENT_INVALID";
|
|
12579
|
-
constructor() {
|
|
12580
|
-
super("OpenAPI document is invalid or could not be loaded.");
|
|
12581
|
-
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
|
+
});
|
|
12582
12538
|
}
|
|
12583
12539
|
}
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
}
|
|
12611
|
-
function arrayValue(value) {
|
|
12612
|
-
return Array.isArray(value) ? value : [];
|
|
12613
|
-
}
|
|
12614
|
-
function sortedKeys(value) {
|
|
12615
|
-
return value ? Object.keys(value).sort() : [];
|
|
12616
|
-
}
|
|
12617
|
-
function sameJson(left, right) {
|
|
12618
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
12619
|
-
}
|
|
12620
|
-
function decodeJsonPointerPart(value) {
|
|
12621
|
-
return value.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12622
|
-
}
|
|
12623
|
-
function resolveSchema(value, document, seen = new Set) {
|
|
12624
|
-
const schema = recordValue(value);
|
|
12625
|
-
if (!schema)
|
|
12626
|
-
return;
|
|
12627
|
-
const ref = stringValue(schema.$ref);
|
|
12628
|
-
if (!ref || !ref.startsWith("#/components/schemas/"))
|
|
12629
|
-
return schema;
|
|
12630
|
-
if (seen.has(ref))
|
|
12631
|
-
return schema;
|
|
12632
|
-
const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
|
|
12633
|
-
const components = recordValue(document.components);
|
|
12634
|
-
const schemas = recordValue(components?.schemas);
|
|
12635
|
-
const target = schemas?.[name];
|
|
12636
|
-
if (target === undefined)
|
|
12637
|
-
return schema;
|
|
12638
|
-
return resolveSchema(target, document, new Set([...seen, ref]));
|
|
12639
|
-
}
|
|
12640
|
-
function schemaEnum(value) {
|
|
12641
|
-
return Array.isArray(value?.enum) ? value.enum : undefined;
|
|
12642
|
-
}
|
|
12643
|
-
function schemaProperties(value) {
|
|
12644
|
-
return recordValue(value?.properties);
|
|
12645
|
-
}
|
|
12646
|
-
function schemaRequired(value) {
|
|
12647
|
-
return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
|
|
12648
|
-
}
|
|
12649
|
-
function schemaType(value) {
|
|
12650
|
-
return value?.type;
|
|
12651
|
-
}
|
|
12652
|
-
function addChange(changes, change) {
|
|
12653
|
-
if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
|
|
12654
|
-
return;
|
|
12655
|
-
changes.push(change);
|
|
12656
|
-
}
|
|
12657
|
-
function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
|
|
12658
|
-
const baseSchema = resolveSchema(baseValue, baseDocument);
|
|
12659
|
-
const currentSchema = resolveSchema(currentValue, currentDocument);
|
|
12660
|
-
if (!baseSchema || !currentSchema)
|
|
12661
|
-
return;
|
|
12662
|
-
const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
|
|
12663
|
-
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
|
+
}
|
|
12664
12566
|
return;
|
|
12665
|
-
|
|
12666
|
-
if (
|
|
12567
|
+
}
|
|
12568
|
+
if (currentBody.required === true && baseBody.required !== true) {
|
|
12667
12569
|
addChange(changes, {
|
|
12668
12570
|
kind: "breaking",
|
|
12669
|
-
code: "
|
|
12670
|
-
path
|
|
12671
|
-
message: "
|
|
12571
|
+
code: "request-body-required",
|
|
12572
|
+
path: `${path}.requestBody`,
|
|
12573
|
+
message: "An optional request body became required."
|
|
12672
12574
|
});
|
|
12673
12575
|
}
|
|
12674
|
-
const
|
|
12675
|
-
const
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
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;
|
|
12687
12589
|
}
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
const currentProperties = schemaProperties(currentSchema);
|
|
12691
|
-
if (baseProperties && currentProperties) {
|
|
12692
|
-
for (const name of sortedKeys(baseProperties)) {
|
|
12693
|
-
if (currentProperties[name] === undefined) {
|
|
12694
|
-
const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
|
|
12695
|
-
if (direction === "output" || inputRemovalIsBreaking) {
|
|
12696
|
-
addChange(changes, {
|
|
12697
|
-
kind: "breaking",
|
|
12698
|
-
code: direction === "output" ? "response-property-removed" : "request-property-removed",
|
|
12699
|
-
path: `${path}.properties.${name}`,
|
|
12700
|
-
message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
|
|
12701
|
-
});
|
|
12702
|
-
}
|
|
12703
|
-
continue;
|
|
12704
|
-
}
|
|
12705
|
-
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);
|
|
12706
12592
|
}
|
|
12707
12593
|
}
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
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) {
|
|
12713
12616
|
addChange(changes, {
|
|
12714
12617
|
kind: "breaking",
|
|
12715
|
-
code: "
|
|
12716
|
-
path: `${path}.
|
|
12717
|
-
message:
|
|
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."
|
|
12718
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);
|
|
12719
12626
|
}
|
|
12720
12627
|
}
|
|
12721
|
-
|
|
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;
|
|
12722
12664
|
addChange(changes, {
|
|
12723
12665
|
kind: "breaking",
|
|
12724
|
-
code: "
|
|
12725
|
-
path
|
|
12726
|
-
message: "
|
|
12666
|
+
code: "component-removed",
|
|
12667
|
+
path: `components.${group}.${name}`,
|
|
12668
|
+
message: "A reusable component from the previous contract was removed."
|
|
12727
12669
|
});
|
|
12728
12670
|
}
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
}
|
|
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
|
+
});
|
|
12739
12680
|
}
|
|
12740
12681
|
}
|
|
12741
12682
|
}
|
|
12742
|
-
function
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
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 };
|
|
12747
12688
|
}
|
|
12748
|
-
function
|
|
12749
|
-
const
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
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;
|
|
12757
12707
|
}
|
|
12758
|
-
return result;
|
|
12759
12708
|
}
|
|
12760
|
-
function
|
|
12761
|
-
|
|
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 };
|
|
12762
12728
|
}
|
|
12763
|
-
function
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
const
|
|
12768
|
-
|
|
12769
|
-
|
|
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) {
|
|
12770
12757
|
addChange(changes, {
|
|
12771
12758
|
kind: "breaking",
|
|
12772
|
-
code: "
|
|
12773
|
-
path:
|
|
12774
|
-
message: "A
|
|
12759
|
+
code: "path-removed",
|
|
12760
|
+
path: `paths.${path}`,
|
|
12761
|
+
message: "A path from the previous contract was removed."
|
|
12775
12762
|
});
|
|
12776
12763
|
continue;
|
|
12777
12764
|
}
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
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);
|
|
12788
12787
|
}
|
|
12789
12788
|
}
|
|
12790
|
-
for (const
|
|
12791
|
-
if (
|
|
12792
|
-
continue;
|
|
12793
|
-
const parameter = currentParameters.get(key);
|
|
12794
|
-
if (!parameter)
|
|
12789
|
+
for (const path of sortedKeys(currentPaths)) {
|
|
12790
|
+
if (basePaths[path] !== undefined)
|
|
12795
12791
|
continue;
|
|
12796
12792
|
addChange(changes, {
|
|
12797
|
-
kind:
|
|
12798
|
-
code:
|
|
12799
|
-
path:
|
|
12800
|
-
message:
|
|
12793
|
+
kind: "non-breaking",
|
|
12794
|
+
code: "path-added",
|
|
12795
|
+
path: `paths.${path}`,
|
|
12796
|
+
message: "A path was added."
|
|
12801
12797
|
});
|
|
12802
12798
|
}
|
|
12799
|
+
compareComponents(baseDocument, currentDocument, changes);
|
|
12800
|
+
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
12801
|
+
return { ok: breaking.length === 0, breaking, changes };
|
|
12803
12802
|
}
|
|
12804
|
-
function
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
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."
|
|
12821
12870
|
});
|
|
12822
|
-
}
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
code: "
|
|
12826
|
-
|
|
12827
|
-
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."
|
|
12828
12879
|
});
|
|
12829
12880
|
}
|
|
12830
|
-
|
|
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
|
+
});
|
|
12831
12888
|
}
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
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."
|
|
12838
12914
|
});
|
|
12839
12915
|
}
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
const
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
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."
|
|
12851
12949
|
});
|
|
12852
|
-
continue;
|
|
12853
|
-
}
|
|
12854
|
-
if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
|
|
12855
|
-
compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
|
|
12856
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
|
+
});
|
|
12857
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
|
+
};
|
|
12858
12989
|
}
|
|
12859
|
-
function
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
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]);
|
|
12873
13052
|
}
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
code: "response-media-type-removed",
|
|
12883
|
-
path: `${path}.responses.${status}.content.${mediaType}`,
|
|
12884
|
-
message: "A response media type from the previous contract was removed."
|
|
12885
|
-
});
|
|
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)
|
|
12886
13061
|
continue;
|
|
12887
|
-
|
|
12888
|
-
if (
|
|
12889
|
-
|
|
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
|
+
}
|
|
12890
13072
|
}
|
|
12891
13073
|
}
|
|
12892
13074
|
}
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
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
|
+
}
|
|
12902
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
|
+
};
|
|
12903
13117
|
}
|
|
12904
|
-
function
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
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
|
+
]);
|
|
12918
13178
|
}
|
|
12919
|
-
function
|
|
12920
|
-
const
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
code: "component-added",
|
|
12941
|
-
path: `components.${group}.${name}`,
|
|
12942
|
-
message: "A reusable component was added."
|
|
12943
|
-
});
|
|
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"
|
|
12944
13200
|
}
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
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
|
+
};
|
|
12952
13208
|
}
|
|
12953
|
-
function
|
|
12954
|
-
const
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
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
|
+
`);
|
|
12962
13222
|
}
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
}
|
|
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
|
+
`);
|
|
12972
13239
|
}
|
|
12973
|
-
|
|
12974
|
-
const
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
if (await readFile10(path, "utf8") === content)
|
|
12979
|
-
return { path, written: false };
|
|
12980
|
-
} catch {}
|
|
12981
|
-
const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
12982
|
-
try {
|
|
12983
|
-
await writeFile5(temporaryPath, content, "utf8");
|
|
12984
|
-
await rename6(temporaryPath, path);
|
|
12985
|
-
} catch (error) {
|
|
12986
|
-
await unlink3(temporaryPath).catch(() => {
|
|
12987
|
-
return;
|
|
12988
|
-
});
|
|
12989
|
-
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 };
|
|
12990
13245
|
}
|
|
12991
|
-
return
|
|
13246
|
+
return;
|
|
12992
13247
|
}
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
if (error instanceof OpenApiDocumentError)
|
|
13003
|
-
throw error;
|
|
13004
|
-
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
|
+
}
|
|
13005
13257
|
}
|
|
13258
|
+
return lines.join(`
|
|
13259
|
+
`);
|
|
13006
13260
|
}
|
|
13007
|
-
|
|
13008
|
-
const
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
const
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
for (const path of sortedKeys(basePaths)) {
|
|
13018
|
-
const basePathItem = recordValue(basePaths[path]);
|
|
13019
|
-
const currentPathItem = recordValue(currentPaths[path]);
|
|
13020
|
-
if (!basePathItem || !currentPathItem) {
|
|
13021
|
-
addChange(changes, {
|
|
13022
|
-
kind: "breaking",
|
|
13023
|
-
code: "path-removed",
|
|
13024
|
-
path: `paths.${path}`,
|
|
13025
|
-
message: "A path from the previous contract was removed."
|
|
13026
|
-
});
|
|
13027
|
-
continue;
|
|
13028
|
-
}
|
|
13029
|
-
for (const method of HTTP_METHODS) {
|
|
13030
|
-
const baseOperation = recordValue(basePathItem[method]);
|
|
13031
|
-
const currentOperation = recordValue(currentPathItem[method]);
|
|
13032
|
-
if (!baseOperation || !currentOperation) {
|
|
13033
|
-
if (baseOperation) {
|
|
13034
|
-
addChange(changes, {
|
|
13035
|
-
kind: "breaking",
|
|
13036
|
-
code: "operation-removed",
|
|
13037
|
-
path: `paths.${path}.${method}`,
|
|
13038
|
-
message: "An operation from the previous contract was removed."
|
|
13039
|
-
});
|
|
13040
|
-
} else if (currentOperation) {
|
|
13041
|
-
addChange(changes, {
|
|
13042
|
-
kind: "non-breaking",
|
|
13043
|
-
code: "operation-added",
|
|
13044
|
-
path: `paths.${path}.${method}`,
|
|
13045
|
-
message: "An operation was added to an existing path."
|
|
13046
|
-
});
|
|
13047
|
-
}
|
|
13048
|
-
continue;
|
|
13049
|
-
}
|
|
13050
|
-
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}";`);
|
|
13051
13271
|
}
|
|
13052
13272
|
}
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
continue;
|
|
13056
|
-
addChange(changes, {
|
|
13057
|
-
kind: "non-breaking",
|
|
13058
|
-
code: "path-added",
|
|
13059
|
-
path: `paths.${path}`,
|
|
13060
|
-
message: "A path was added."
|
|
13061
|
-
});
|
|
13062
|
-
}
|
|
13063
|
-
compareComponents(baseDocument, currentDocument, changes);
|
|
13064
|
-
const breaking = changes.filter((change) => change.kind === "breaking");
|
|
13065
|
-
return { ok: breaking.length === 0, breaking, changes };
|
|
13066
|
-
}
|
|
13067
|
-
function formatOpenApiDiff(result) {
|
|
13068
|
-
if (result.changes.length === 0)
|
|
13069
|
-
return "OpenAPI diff passed: no contract changes.";
|
|
13070
|
-
return [
|
|
13071
|
-
`OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
|
|
13072
|
-
...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
|
|
13073
|
-
].join(`
|
|
13273
|
+
lines.push("}");
|
|
13274
|
+
return lines.join(`
|
|
13074
13275
|
`);
|
|
13075
13276
|
}
|
|
13277
|
+
|
|
13278
|
+
// src/index.ts
|
|
13279
|
+
init_generate();
|
|
13280
|
+
init_contract_manifest();
|
|
13281
|
+
|
|
13076
13282
|
// src/config.ts
|
|
13077
13283
|
init_graphql_options();
|
|
13078
13284
|
import { existsSync as existsSync5 } from "node:fs";
|
|
13079
|
-
import { join as
|
|
13285
|
+
import { join as join9, resolve as resolve15 } from "node:path";
|
|
13080
13286
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
13081
13287
|
var DEFAULT_SUPACLOUD_CONFIG = {
|
|
13082
13288
|
graphql: false,
|
|
@@ -13169,8 +13375,8 @@ function validateGovernanceConfig(config) {
|
|
|
13169
13375
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
13170
13376
|
const resolved = defineSupacloudConfig(config);
|
|
13171
13377
|
return {
|
|
13172
|
-
rootDir:
|
|
13173
|
-
outDir:
|
|
13378
|
+
rootDir: resolve15(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
|
|
13379
|
+
outDir: resolve15(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
|
|
13174
13380
|
include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
13175
13381
|
strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
|
|
13176
13382
|
requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
|
|
@@ -13188,16 +13394,16 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
13188
13394
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
13189
13395
|
graphql: resolved.graphql ? {
|
|
13190
13396
|
...resolved.graphql,
|
|
13191
|
-
schema:
|
|
13397
|
+
schema: resolve15(cwd, resolved.graphql.schema)
|
|
13192
13398
|
} : undefined
|
|
13193
13399
|
};
|
|
13194
13400
|
}
|
|
13195
13401
|
async function loadSupacloudConfig(cwd = process.cwd()) {
|
|
13196
13402
|
const candidates = [
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13403
|
+
join9(cwd, "supacloud.config.ts"),
|
|
13404
|
+
join9(cwd, "supacloud.config.mts"),
|
|
13405
|
+
join9(cwd, "supacloud.config.js"),
|
|
13406
|
+
join9(cwd, "supacloud.config.mjs")
|
|
13201
13407
|
];
|
|
13202
13408
|
const configPath = candidates.find((candidate) => existsSync5(candidate));
|
|
13203
13409
|
if (!configPath)
|
|
@@ -13234,6 +13440,7 @@ export {
|
|
|
13234
13440
|
TraitCompiler,
|
|
13235
13441
|
analyzeProject,
|
|
13236
13442
|
applyDiagnosticFix,
|
|
13443
|
+
assessMigration,
|
|
13237
13444
|
buildContractManifest,
|
|
13238
13445
|
buildDeliveryProject,
|
|
13239
13446
|
camelName,
|
|
@@ -13256,6 +13463,7 @@ export {
|
|
|
13256
13463
|
exportGraphMermaid,
|
|
13257
13464
|
formatDeliveryPlan,
|
|
13258
13465
|
formatGraph,
|
|
13466
|
+
formatMigrationAssessment,
|
|
13259
13467
|
formatOpenApiDiff,
|
|
13260
13468
|
generateApplication,
|
|
13261
13469
|
generateDatabaseContracts,
|