contract-drift-detection 0.1.4 → 0.1.6
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/CHANGELOG.md +26 -0
- package/dist/cli.js +223 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +162 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.6 - 2026-03-16
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
|
|
9
|
+
- Drift detection alerts now render as clean, colorized multiline terminal output instead of JSON-escaped logger lines.
|
|
10
|
+
- Preserved structured drift payload logging at debug level for observability without cluttering normal terminal UX.
|
|
11
|
+
- Updated drift proxy test assertions to validate pretty stderr output behavior.
|
|
12
|
+
|
|
13
|
+
## 0.1.5 - 2026-03-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- New drift reporting outputs via `--reporter pretty|json|junit`.
|
|
18
|
+
- Optional report destination via `--report-file <path>`.
|
|
19
|
+
- Strict drift mode via `--strict` to enforce `additionalProperties: false` behavior during validation.
|
|
20
|
+
- Path-based drift ignore controls via repeated `--drift-ignore <path>` and `.driftignore` support.
|
|
21
|
+
- New diagnostics endpoint: `GET /__drift` for in-memory drift issue visibility.
|
|
22
|
+
- New unit test coverage for drift analyzer strict/ignore behavior.
|
|
23
|
+
- New CLI e2e coverage for discover failure UX.
|
|
24
|
+
|
|
25
|
+
### Improved
|
|
26
|
+
|
|
27
|
+
- CI defaults now support fail-fast drift behavior with `--fail-on-drift`.
|
|
28
|
+
- Drift validation pipeline now supports machine-readable reporting for CI gates.
|
|
29
|
+
- Server-level drift callback flow for automated fail/report handling.
|
|
30
|
+
|
|
5
31
|
## 0.1.4 - 2026-03-16
|
|
6
32
|
|
|
7
33
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import process2 from "process";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
7
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
8
8
|
import path2 from "path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
@@ -238,11 +238,26 @@ function resolvePathFromCwd(cwd, value) {
|
|
|
238
238
|
return path2.isAbsolute(value) ? value : path2.join(cwd, value);
|
|
239
239
|
}
|
|
240
240
|
function applyServeOptions(command) {
|
|
241
|
-
return command.option("--spec <path>", "Path to an OpenAPI 3.x file").option("--spec-url <url>", "Remote URL to an OpenAPI 3.x file").option("--discover <backend-url>", "Discover OpenAPI from a backend URL and cache it locally").option("--port <port>", "Port to bind the server", "4010").option("--host <host>", "Host to bind the server", "0.0.0.0").option("--db <path>", "JSON database path", ".mock-db.json").option("--cors-origin <origin>", "CORS origin (default is *)", "*").option("--drift-check <url>", "Forward traffic to a real backend and validate responses").option("--fallback-to-mock", "Fallback to mock responses when proxying fails", false).option("--verbose", "Enable verbose logging", false);
|
|
241
|
+
return command.option("--spec <path>", "Path to an OpenAPI 3.x file").option("--spec-url <url>", "Remote URL to an OpenAPI 3.x file").option("--discover <backend-url>", "Discover OpenAPI from a backend URL and cache it locally").option("--port <port>", "Port to bind the server", "4010").option("--host <host>", "Host to bind the server", "0.0.0.0").option("--db <path>", "JSON database path", ".mock-db.json").option("--cors-origin <origin>", "CORS origin (default is *)", "*").option("--drift-check <url>", "Forward traffic to a real backend and validate responses").option("--strict", "Treat additional fields as drift", false).option("--drift-ignore <paths>", "Comma-separated JSON pointer paths to ignore during drift checks").option("--reporter <type>", "Drift reporter: pretty|json|junit", "pretty").option("--report-file <path>", "Output file path for json/junit reporters").option("--fail-on-drift", "Exit with code 1 when drift is detected (enabled by default in CI)", false).option("--fallback-to-mock", "Fallback to mock responses when proxying fails", false).option("--verbose", "Enable verbose logging", false);
|
|
242
|
+
}
|
|
243
|
+
function parseCsvOption(value) {
|
|
244
|
+
if (!value || typeof value !== "string") {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
248
|
+
}
|
|
249
|
+
async function loadDriftIgnoreFromFile(cwd) {
|
|
250
|
+
const ignoreFilePath = path2.join(cwd, ".driftignore");
|
|
251
|
+
try {
|
|
252
|
+
const raw = await readFile(ignoreFilePath, "utf8");
|
|
253
|
+
return raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
242
257
|
}
|
|
243
258
|
function createCli() {
|
|
244
259
|
const program = new Command();
|
|
245
|
-
program.name("contract-drift-detection").description("Stateful OpenAPI mock server with contract drift detection").version("0.1.
|
|
260
|
+
program.name("contract-drift-detection").description("Stateful OpenAPI mock server with contract drift detection").version("0.1.6");
|
|
246
261
|
applyServeOptions(program);
|
|
247
262
|
applyServeOptions(program.command("serve").description("Start the mock engine"));
|
|
248
263
|
program.command("init").description("Create a starter config for the current workspace").option("--spec <path>", "Default OpenAPI path", "openapi.yaml").option("--template <name>", "Template to generate (rest-crud | none)", "rest-crud").option("--db <path>", "Default JSON database path", ".mock-db.json").option("--port <port>", "Default port", "4010").option("--host <host>", "Default host", "0.0.0.0");
|
|
@@ -262,6 +277,13 @@ async function resolveServeConfig(cwd, options) {
|
|
|
262
277
|
if (!resolvedSpecPath) {
|
|
263
278
|
throw new Error("Provide one of --spec, --spec-url, or --discover");
|
|
264
279
|
}
|
|
280
|
+
const ignoreFromCli = parseCsvOption(options.driftIgnore);
|
|
281
|
+
const ignoreFromFile = await loadDriftIgnoreFromFile(cwd);
|
|
282
|
+
const mergedIgnore = Array.from(/* @__PURE__ */ new Set([...ignoreFromFile, ...ignoreFromCli]));
|
|
283
|
+
const reporter = String(options.reporter ?? "pretty");
|
|
284
|
+
if (!["pretty", "json", "junit"].includes(reporter)) {
|
|
285
|
+
throw new Error(`Invalid reporter '${reporter}'. Use one of: pretty, json, junit`);
|
|
286
|
+
}
|
|
265
287
|
return {
|
|
266
288
|
specPath: resolvedSpecPath,
|
|
267
289
|
port: Number(options.port ?? 4010),
|
|
@@ -269,6 +291,11 @@ async function resolveServeConfig(cwd, options) {
|
|
|
269
291
|
dbPath: resolvePathFromCwd(cwd, String(options.db ?? ".mock-db.json")),
|
|
270
292
|
corsOrigin: String(options.corsOrigin ?? "*"),
|
|
271
293
|
driftCheckTarget: options.driftCheck ? String(options.driftCheck) : void 0,
|
|
294
|
+
strictDrift: Boolean(options.strict),
|
|
295
|
+
driftIgnorePaths: mergedIgnore,
|
|
296
|
+
reporter,
|
|
297
|
+
reportFile: options.reportFile ? resolvePathFromCwd(cwd, String(options.reportFile)) : void 0,
|
|
298
|
+
failOnDrift: Boolean(options.failOnDrift) || process.env.CI === "true",
|
|
272
299
|
fallbackToMockOnProxyError: Boolean(options.fallbackToMock),
|
|
273
300
|
verbose: Boolean(options.verbose)
|
|
274
301
|
};
|
|
@@ -298,7 +325,7 @@ async function writeStarterConfig(cwd, initConfig) {
|
|
|
298
325
|
}
|
|
299
326
|
|
|
300
327
|
// src/server.ts
|
|
301
|
-
import
|
|
328
|
+
import path6 from "path";
|
|
302
329
|
import Fastify from "fastify";
|
|
303
330
|
import cors from "@fastify/cors";
|
|
304
331
|
|
|
@@ -453,39 +480,112 @@ function applyDslMutation(extension, request, collection, defaultIdKey) {
|
|
|
453
480
|
import Ajv from "ajv";
|
|
454
481
|
import addFormats from "ajv-formats";
|
|
455
482
|
import pc from "picocolors";
|
|
483
|
+
function deepClone2(value) {
|
|
484
|
+
return JSON.parse(JSON.stringify(value));
|
|
485
|
+
}
|
|
486
|
+
function pathMatchesIgnoreRule(instancePath, rule) {
|
|
487
|
+
if (!rule) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const pathSegments = instancePath.split("/").filter(Boolean);
|
|
491
|
+
const ruleSegments = rule.split("/").filter(Boolean);
|
|
492
|
+
if (ruleSegments.length > pathSegments.length) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return ruleSegments.every((segment, index) => segment === "*" || segment === pathSegments[index]);
|
|
496
|
+
}
|
|
497
|
+
function applyStrictAdditionalProperties(schema) {
|
|
498
|
+
const output = deepClone2(schema);
|
|
499
|
+
const walk = (candidate) => {
|
|
500
|
+
if (candidate.type === "object" || candidate.properties) {
|
|
501
|
+
if (candidate.additionalProperties === void 0) {
|
|
502
|
+
candidate.additionalProperties = false;
|
|
503
|
+
}
|
|
504
|
+
for (const value of Object.values(candidate.properties ?? {})) {
|
|
505
|
+
if (value && !("$ref" in value)) {
|
|
506
|
+
walk(value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (candidate.type === "array" && candidate.items && !("$ref" in candidate.items)) {
|
|
511
|
+
walk(candidate.items);
|
|
512
|
+
}
|
|
513
|
+
for (const entry of candidate.allOf ?? []) {
|
|
514
|
+
if (!("$ref" in entry)) {
|
|
515
|
+
walk(entry);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const entry of candidate.oneOf ?? []) {
|
|
519
|
+
if (!("$ref" in entry)) {
|
|
520
|
+
walk(entry);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const entry of candidate.anyOf ?? []) {
|
|
524
|
+
if (!("$ref" in entry)) {
|
|
525
|
+
walk(entry);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
walk(output);
|
|
530
|
+
return output;
|
|
531
|
+
}
|
|
532
|
+
function buildAjvSchema(schema, strictMode) {
|
|
533
|
+
return strictMode ? applyStrictAdditionalProperties(schema) : schema;
|
|
534
|
+
}
|
|
456
535
|
function formatErrors(errors) {
|
|
457
536
|
return (errors ?? []).map((error) => {
|
|
458
537
|
const location = error.instancePath || "/";
|
|
459
538
|
return `${location} ${error.message ?? "failed validation"}`.trim();
|
|
460
539
|
});
|
|
461
540
|
}
|
|
541
|
+
function analyzeDrift(ajv, schema, body, options) {
|
|
542
|
+
const normalizedSchema = buildAjvSchema(schema, options.strictMode);
|
|
543
|
+
const validate = ajv.compile(normalizedSchema);
|
|
544
|
+
const valid = validate(body);
|
|
545
|
+
if (valid) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
const filteredErrors = (validate.errors ?? []).filter(
|
|
549
|
+
(entry) => !options.ignorePaths.some((rule) => pathMatchesIgnoreRule(entry.instancePath || "/", rule))
|
|
550
|
+
);
|
|
551
|
+
return formatErrors(filteredErrors);
|
|
552
|
+
}
|
|
462
553
|
var DriftDetector = class {
|
|
463
|
-
constructor(logger) {
|
|
554
|
+
constructor(logger, options) {
|
|
464
555
|
this.logger = logger;
|
|
465
556
|
addFormats(this.ajv);
|
|
557
|
+
this.strictMode = options?.strictMode ?? false;
|
|
558
|
+
this.ignorePaths = options?.ignorePaths ?? [];
|
|
466
559
|
}
|
|
467
560
|
ajv = new Ajv({ allErrors: true, strict: false });
|
|
468
|
-
|
|
561
|
+
strictMode;
|
|
562
|
+
ignorePaths;
|
|
563
|
+
validate(method, path7, statusCode, schema, body) {
|
|
469
564
|
if (!schema || body === void 0 || body === null) {
|
|
470
565
|
return null;
|
|
471
566
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
567
|
+
const errors = analyzeDrift(this.ajv, schema, body, {
|
|
568
|
+
strictMode: this.strictMode,
|
|
569
|
+
ignorePaths: this.ignorePaths
|
|
570
|
+
});
|
|
571
|
+
if (!errors.length) {
|
|
475
572
|
return null;
|
|
476
573
|
}
|
|
477
574
|
const issue = {
|
|
478
575
|
method: method.toUpperCase(),
|
|
479
|
-
path:
|
|
576
|
+
path: path7,
|
|
480
577
|
statusCode,
|
|
481
|
-
message: `Drift detected for ${method.toUpperCase()} ${
|
|
482
|
-
errors
|
|
578
|
+
message: `Drift detected for ${method.toUpperCase()} ${path7} (${statusCode})`,
|
|
579
|
+
errors
|
|
483
580
|
};
|
|
484
581
|
const errorLines = issue.errors.map((entry) => ` \u2022 ${entry}`).join("\n");
|
|
485
|
-
|
|
582
|
+
const prettyBlock = [
|
|
486
583
|
`${pc.bgRed(pc.black(" \u{1F6A8} DRIFT DETECTED "))} ${pc.bold(issue.method)} ${issue.path} (${statusCode})`,
|
|
487
584
|
errorLines
|
|
488
|
-
].join("\n")
|
|
585
|
+
].join("\n");
|
|
586
|
+
process.stderr.write(`${prettyBlock}
|
|
587
|
+
`);
|
|
588
|
+
this.logger.debug({ drift: issue }, issue.message);
|
|
489
589
|
return issue;
|
|
490
590
|
}
|
|
491
591
|
};
|
|
@@ -635,7 +735,7 @@ async function loadOpenApiDocument(specPath) {
|
|
|
635
735
|
}
|
|
636
736
|
|
|
637
737
|
// src/state-store.ts
|
|
638
|
-
import { mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
|
|
738
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
639
739
|
import path4 from "path";
|
|
640
740
|
function normalizeDatabase(input) {
|
|
641
741
|
return {
|
|
@@ -676,7 +776,7 @@ var JsonStateStore = class {
|
|
|
676
776
|
}
|
|
677
777
|
}
|
|
678
778
|
async read() {
|
|
679
|
-
const raw = await
|
|
779
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
680
780
|
return normalizeDatabase(JSON.parse(raw));
|
|
681
781
|
}
|
|
682
782
|
async write(database) {
|
|
@@ -692,8 +792,8 @@ var JsonStateStore = class {
|
|
|
692
792
|
};
|
|
693
793
|
|
|
694
794
|
// src/proxy.ts
|
|
695
|
-
async function proxyRequest(targetBaseUrl,
|
|
696
|
-
const response = await fetch(new URL(
|
|
795
|
+
async function proxyRequest(targetBaseUrl, path7, init) {
|
|
796
|
+
const response = await fetch(new URL(path7, targetBaseUrl), init);
|
|
697
797
|
const contentType = readContentType(response.headers);
|
|
698
798
|
let body;
|
|
699
799
|
let rawBody;
|
|
@@ -714,6 +814,56 @@ async function proxyRequest(targetBaseUrl, path6, init) {
|
|
|
714
814
|
};
|
|
715
815
|
}
|
|
716
816
|
|
|
817
|
+
// src/drift-reporter.ts
|
|
818
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
819
|
+
import path5 from "path";
|
|
820
|
+
function escapeXml(value) {
|
|
821
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
822
|
+
}
|
|
823
|
+
function toJunit(issues) {
|
|
824
|
+
const testCases = issues.map((issue, index) => {
|
|
825
|
+
const name = `${issue.method} ${issue.path} (${issue.statusCode}) #${index + 1}`;
|
|
826
|
+
const failure = escapeXml(issue.errors.join("; "));
|
|
827
|
+
return ` <testcase classname="contract-drift-detection" name="${escapeXml(name)}">
|
|
828
|
+
<failure message="drift detected">${failure}</failure>
|
|
829
|
+
</testcase>`;
|
|
830
|
+
}).join("\n");
|
|
831
|
+
return [
|
|
832
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
833
|
+
`<testsuite name="contract-drift-detection" tests="${issues.length}" failures="${issues.length}">`,
|
|
834
|
+
testCases,
|
|
835
|
+
"</testsuite>",
|
|
836
|
+
""
|
|
837
|
+
].join("\n");
|
|
838
|
+
}
|
|
839
|
+
function toJson(issues) {
|
|
840
|
+
const report = {
|
|
841
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
842
|
+
summary: {
|
|
843
|
+
total: issues.length
|
|
844
|
+
},
|
|
845
|
+
issues
|
|
846
|
+
};
|
|
847
|
+
return `${JSON.stringify(report, null, 2)}
|
|
848
|
+
`;
|
|
849
|
+
}
|
|
850
|
+
function defaultReportPath(config) {
|
|
851
|
+
if (config.reportFile) {
|
|
852
|
+
return config.reportFile;
|
|
853
|
+
}
|
|
854
|
+
return config.reporter === "junit" ? "cdd-drift-report.xml" : "cdd-drift-report.json";
|
|
855
|
+
}
|
|
856
|
+
async function writeDriftReport(config, issues) {
|
|
857
|
+
if (config.reporter === "pretty") {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
const filePath = defaultReportPath(config);
|
|
861
|
+
const content = config.reporter === "junit" ? toJunit(issues) : toJson(issues);
|
|
862
|
+
await mkdir4(path5.dirname(filePath), { recursive: true });
|
|
863
|
+
await writeFile4(filePath, content, "utf8");
|
|
864
|
+
return filePath;
|
|
865
|
+
}
|
|
866
|
+
|
|
717
867
|
// src/server.ts
|
|
718
868
|
function toProxyHeaders(inputHeaders) {
|
|
719
869
|
const passthrough = ["host", "content-length", "connection"];
|
|
@@ -886,7 +1036,7 @@ async function handleMockRoute(store, route, request) {
|
|
|
886
1036
|
}
|
|
887
1037
|
});
|
|
888
1038
|
}
|
|
889
|
-
async function handleProxyRoute(config,
|
|
1039
|
+
async function handleProxyRoute(config, request) {
|
|
890
1040
|
const targetBaseUrl = config.driftCheckTarget;
|
|
891
1041
|
if (!targetBaseUrl) {
|
|
892
1042
|
throw new Error("Proxy target is not configured");
|
|
@@ -900,15 +1050,6 @@ async function handleProxyRoute(config, route, detector, request) {
|
|
|
900
1050
|
headers,
|
|
901
1051
|
body: toProxyBody(request)
|
|
902
1052
|
});
|
|
903
|
-
if (result.statusCode >= 200 && result.statusCode < 300) {
|
|
904
|
-
detector.validate(
|
|
905
|
-
route.method,
|
|
906
|
-
route.path,
|
|
907
|
-
result.statusCode,
|
|
908
|
-
route.successResponse?.schema,
|
|
909
|
-
result.body
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
1053
|
return {
|
|
913
1054
|
statusCode: result.statusCode,
|
|
914
1055
|
headers: Object.fromEntries(result.headers.entries()),
|
|
@@ -917,7 +1058,11 @@ async function handleProxyRoute(config, route, detector, request) {
|
|
|
917
1058
|
}
|
|
918
1059
|
async function registerRoutes(app, document, config, store) {
|
|
919
1060
|
const routes = buildRouteContexts(document);
|
|
920
|
-
const detector = new DriftDetector(app.log
|
|
1061
|
+
const detector = new DriftDetector(app.log, {
|
|
1062
|
+
strictMode: config.strictDrift,
|
|
1063
|
+
ignorePaths: config.driftIgnorePaths
|
|
1064
|
+
});
|
|
1065
|
+
const driftIssues = [];
|
|
921
1066
|
for (const route of routes) {
|
|
922
1067
|
app.route({
|
|
923
1068
|
method: route.method.toUpperCase(),
|
|
@@ -925,7 +1070,21 @@ async function registerRoutes(app, document, config, store) {
|
|
|
925
1070
|
handler: async (request, reply) => {
|
|
926
1071
|
if (config.driftCheckTarget) {
|
|
927
1072
|
try {
|
|
928
|
-
const proxied = await handleProxyRoute(config,
|
|
1073
|
+
const proxied = await handleProxyRoute(config, request);
|
|
1074
|
+
if (proxied.statusCode >= 200 && proxied.statusCode < 300 && proxied.body !== void 0) {
|
|
1075
|
+
const issue = detector.validate(
|
|
1076
|
+
route.method,
|
|
1077
|
+
route.path,
|
|
1078
|
+
proxied.statusCode,
|
|
1079
|
+
route.successResponse?.schema,
|
|
1080
|
+
proxied.body
|
|
1081
|
+
);
|
|
1082
|
+
if (issue) {
|
|
1083
|
+
driftIssues.push(issue);
|
|
1084
|
+
await writeDriftReport(config, driftIssues);
|
|
1085
|
+
await config.onDriftDetected?.(issue);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
929
1088
|
for (const [headerName, headerValue] of Object.entries(proxied.headers)) {
|
|
930
1089
|
if (headerName.toLowerCase() === "content-length") {
|
|
931
1090
|
continue;
|
|
@@ -955,6 +1114,10 @@ async function registerRoutes(app, document, config, store) {
|
|
|
955
1114
|
hasDsl: Boolean(route.operation["x-mock-state"])
|
|
956
1115
|
}))
|
|
957
1116
|
);
|
|
1117
|
+
app.get("/__drift", async () => ({
|
|
1118
|
+
total: driftIssues.length,
|
|
1119
|
+
issues: driftIssues
|
|
1120
|
+
}));
|
|
958
1121
|
}
|
|
959
1122
|
async function createServer(config) {
|
|
960
1123
|
const app = Fastify({
|
|
@@ -970,7 +1133,7 @@ async function createServer(config) {
|
|
|
970
1133
|
});
|
|
971
1134
|
const document = await loadOpenApiDocument(config.specPath);
|
|
972
1135
|
const seedCollections = inferSeedCollections(document);
|
|
973
|
-
const dbPath = resolveFile(
|
|
1136
|
+
const dbPath = resolveFile(path6.dirname(config.specPath), config.dbPath);
|
|
974
1137
|
const store = new JsonStateStore(dbPath);
|
|
975
1138
|
await store.initialize(seedCollections);
|
|
976
1139
|
app.get("/__health", async () => ({ status: "ok" }));
|
|
@@ -986,8 +1149,12 @@ function renderStartupBanner(config) {
|
|
|
986
1149
|
`- Spec: ${config.specPath}`,
|
|
987
1150
|
`- DB: ${config.dbPath}`,
|
|
988
1151
|
`- Mode: ${config.driftCheckTarget ? `proxy + drift-check (${config.driftCheckTarget})` : "stateful mock"}`,
|
|
1152
|
+
`- Drift policy: strict=${config.strictDrift ? "on" : "off"}, fail-on-drift=${config.failOnDrift ? "on" : "off"}`,
|
|
1153
|
+
`- Reporter: ${config.reporter}${config.reportFile ? ` (${config.reportFile})` : ""}`,
|
|
1154
|
+
`- Ignore rules: ${config.driftIgnorePaths.length ? config.driftIgnorePaths.join(", ") : "none"}`,
|
|
989
1155
|
"- Health: GET /__health",
|
|
990
|
-
"- Routes: GET /__routes"
|
|
1156
|
+
"- Routes: GET /__routes",
|
|
1157
|
+
"- Drift: GET /__drift"
|
|
991
1158
|
];
|
|
992
1159
|
return `${lines.join("\n")}
|
|
993
1160
|
`;
|
|
@@ -998,10 +1165,21 @@ async function main() {
|
|
|
998
1165
|
const initCommand = cli.commands.find((command) => command.name() === "init");
|
|
999
1166
|
const quickstartCommand = cli.commands.find((command) => command.name() === "quickstart");
|
|
1000
1167
|
const startServer = async (rawOptions) => {
|
|
1001
|
-
|
|
1168
|
+
let hasFailedOnDrift = false;
|
|
1169
|
+
const config = await resolveServeConfig(process2.cwd(), rawOptions);
|
|
1170
|
+
config.onDriftDetected = async () => {
|
|
1171
|
+
if (!config.failOnDrift || hasFailedOnDrift) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
hasFailedOnDrift = true;
|
|
1175
|
+
process2.stderr.write("Failing process because drift was detected (--fail-on-drift enabled).\n");
|
|
1176
|
+
setTimeout(() => {
|
|
1177
|
+
process2.exit(1);
|
|
1178
|
+
}, 25);
|
|
1179
|
+
};
|
|
1002
1180
|
const server = await createServer(config);
|
|
1003
1181
|
await server.listen({ port: config.port, host: config.host });
|
|
1004
|
-
|
|
1182
|
+
process2.stdout.write(renderStartupBanner(config));
|
|
1005
1183
|
};
|
|
1006
1184
|
serveCommand?.action(async function() {
|
|
1007
1185
|
await startServer(this.opts());
|
|
@@ -1016,19 +1194,19 @@ async function main() {
|
|
|
1016
1194
|
});
|
|
1017
1195
|
initCommand?.action(async function() {
|
|
1018
1196
|
const options = this.opts();
|
|
1019
|
-
const targetPath = await writeStarterConfig(
|
|
1197
|
+
const targetPath = await writeStarterConfig(process2.cwd(), {
|
|
1020
1198
|
spec: String(options.spec),
|
|
1021
1199
|
db: String(options.db),
|
|
1022
1200
|
host: String(options.host),
|
|
1023
1201
|
port: Number(options.port),
|
|
1024
1202
|
template: options.template === "none" ? "none" : "rest-crud"
|
|
1025
1203
|
});
|
|
1026
|
-
|
|
1204
|
+
process2.stdout.write(`Created ${targetPath}
|
|
1027
1205
|
`);
|
|
1028
1206
|
});
|
|
1029
1207
|
quickstartCommand?.action(async function() {
|
|
1030
1208
|
const options = this.opts();
|
|
1031
|
-
const cwd =
|
|
1209
|
+
const cwd = process2.cwd();
|
|
1032
1210
|
const specPath = String(options.spec ?? "openapi.yaml");
|
|
1033
1211
|
await writeStarterConfig(cwd, {
|
|
1034
1212
|
spec: specPath,
|
|
@@ -1047,22 +1225,22 @@ async function main() {
|
|
|
1047
1225
|
});
|
|
1048
1226
|
const server = await createServer(config);
|
|
1049
1227
|
await server.listen({ port: config.port, host: config.host });
|
|
1050
|
-
|
|
1228
|
+
process2.stdout.write(renderStartupBanner(config));
|
|
1051
1229
|
});
|
|
1052
|
-
await cli.parseAsync(
|
|
1230
|
+
await cli.parseAsync(process2.argv);
|
|
1053
1231
|
}
|
|
1054
1232
|
await main().catch((error) => {
|
|
1055
1233
|
if (error instanceof Error) {
|
|
1056
|
-
|
|
1234
|
+
process2.stderr.write(`Error: ${error.message}
|
|
1057
1235
|
`);
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1236
|
+
if (process2.env.CDD_SHOW_STACK === "1") {
|
|
1237
|
+
process2.stderr.write(`${error.stack}
|
|
1060
1238
|
`);
|
|
1061
1239
|
}
|
|
1062
1240
|
} else {
|
|
1063
|
-
|
|
1241
|
+
process2.stderr.write(`${String(error)}
|
|
1064
1242
|
`);
|
|
1065
1243
|
}
|
|
1066
|
-
|
|
1244
|
+
process2.exitCode = 1;
|
|
1067
1245
|
});
|
|
1068
1246
|
//# sourceMappingURL=cli.js.map
|