@lark-apaas/db-schema-sync 0.1.21 → 0.1.22-alpha.20261015232825
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/dist/bin.d.ts +3 -0
- package/dist/bin.js +285 -18
- package/dist/{chunk-T3RIJCQD.js → chunk-EVOKG4DJ.js} +322 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/bin.d.ts
CHANGED
package/dist/bin.js
CHANGED
|
@@ -4,10 +4,11 @@ import {
|
|
|
4
4
|
__commonJS,
|
|
5
5
|
__require,
|
|
6
6
|
__toESM,
|
|
7
|
+
checkSchemaConsumerCompatibility,
|
|
7
8
|
generateSchema,
|
|
8
9
|
resolveEnvOptions,
|
|
9
10
|
validateGeneratedSchema
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EVOKG4DJ.js";
|
|
11
12
|
|
|
12
13
|
// node_modules/dotenv/package.json
|
|
13
14
|
var require_package = __commonJS({
|
|
@@ -406,8 +407,9 @@ var require_main = __commonJS({
|
|
|
406
407
|
|
|
407
408
|
// src/bin.ts
|
|
408
409
|
var import_dotenv = __toESM(require_main(), 1);
|
|
409
|
-
import {
|
|
410
|
-
import {
|
|
410
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
411
|
+
import { resolve as resolve2 } from "path";
|
|
412
|
+
import { fileURLToPath } from "url";
|
|
411
413
|
|
|
412
414
|
// src/cli-args.ts
|
|
413
415
|
function parseArgs(argv) {
|
|
@@ -428,6 +430,215 @@ function parseArgs(argv) {
|
|
|
428
430
|
};
|
|
429
431
|
}
|
|
430
432
|
|
|
433
|
+
// src/safe-writer.ts
|
|
434
|
+
import { createHash, randomUUID } from "crypto";
|
|
435
|
+
import {
|
|
436
|
+
existsSync,
|
|
437
|
+
mkdirSync,
|
|
438
|
+
readFileSync,
|
|
439
|
+
renameSync,
|
|
440
|
+
rmSync,
|
|
441
|
+
writeFileSync
|
|
442
|
+
} from "fs";
|
|
443
|
+
import { basename, dirname, join, resolve } from "path";
|
|
444
|
+
var SCHEMA_SYNC_CONTRACT = "db-schema-sync.safe-write.v1";
|
|
445
|
+
var MAX_RESULT_MESSAGE_LENGTH = 500;
|
|
446
|
+
function boundedMessage(message) {
|
|
447
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
448
|
+
return normalized.length > MAX_RESULT_MESSAGE_LENGTH ? `${normalized.slice(0, MAX_RESULT_MESSAGE_LENGTH)}...` : normalized;
|
|
449
|
+
}
|
|
450
|
+
function messageFrom(error) {
|
|
451
|
+
return boundedMessage(error instanceof Error ? error.message : String(error));
|
|
452
|
+
}
|
|
453
|
+
function sha256(bytes) {
|
|
454
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
455
|
+
}
|
|
456
|
+
function readTargetSnapshot(outputPath) {
|
|
457
|
+
if (!existsSync(outputPath)) {
|
|
458
|
+
return { bytes: null, sha: null };
|
|
459
|
+
}
|
|
460
|
+
const bytes = readFileSync(outputPath);
|
|
461
|
+
return { bytes, sha: sha256(bytes) };
|
|
462
|
+
}
|
|
463
|
+
function makeResult(status, initial, params = {}) {
|
|
464
|
+
return {
|
|
465
|
+
contract: SCHEMA_SYNC_CONTRACT,
|
|
466
|
+
ok: params.ok ?? false,
|
|
467
|
+
status,
|
|
468
|
+
changed: params.changed ?? false,
|
|
469
|
+
targetShaBefore: initial?.sha ?? null,
|
|
470
|
+
targetShaAfter: params.targetShaAfter === void 0 ? initial?.sha ?? null : params.targetShaAfter,
|
|
471
|
+
removedExportCount: params.removedExportCount ?? 0,
|
|
472
|
+
newDiagnosticCount: params.newDiagnosticCount ?? 0,
|
|
473
|
+
diagnosticSummary: params.diagnosticSummary ?? [],
|
|
474
|
+
...params.message ? { message: boundedMessage(params.message) } : {}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function tempPathFor(outputPath) {
|
|
478
|
+
return join(
|
|
479
|
+
dirname(outputPath),
|
|
480
|
+
`.${basename(outputPath)}.${process.pid}.${randomUUID()}.tmp.ts`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
function hasGeneratedDatabaseObjects(source) {
|
|
484
|
+
return /\bexport\s+const\s+[A-Za-z_$][\w$]*\s*=\s*(?:pgTable|pgView|pgMaterializedView)\s*\(/.test(
|
|
485
|
+
source
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
function validationFailureMessage(validation) {
|
|
489
|
+
return `Generated schema validation failed (${validation.errorType}): ${validation.message ?? validation.status}`;
|
|
490
|
+
}
|
|
491
|
+
function compatibilityFailureResult(initial, compatibility) {
|
|
492
|
+
const common = {
|
|
493
|
+
removedExportCount: compatibility.removedExportCount,
|
|
494
|
+
newDiagnosticCount: compatibility.newDiagnosticCount,
|
|
495
|
+
diagnosticSummary: compatibility.diagnosticSummary,
|
|
496
|
+
message: compatibility.message ?? "Candidate schema consumer compatibility check failed"
|
|
497
|
+
};
|
|
498
|
+
return compatibility.status === "incompatible" ? makeResult("schema_consumer_incompatible", initial, common) : makeResult("schema_compatibility_unavailable", initial, common);
|
|
499
|
+
}
|
|
500
|
+
function writeSchemaFileSafely(options) {
|
|
501
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
502
|
+
const outputPath = resolve(cwd, options.outputPath);
|
|
503
|
+
let initial;
|
|
504
|
+
try {
|
|
505
|
+
initial = readTargetSnapshot(outputPath);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return makeResult("schema_target_read_failed", void 0, {
|
|
508
|
+
message: messageFrom(error)
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return makeResult("schema_candidate_write_failed", initial, {
|
|
515
|
+
message: messageFrom(error)
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const tempPath = options.controls?.createTempPath?.(outputPath) ?? tempPathFor(outputPath);
|
|
519
|
+
const validate = options.validate ?? validateGeneratedSchema;
|
|
520
|
+
const checkCompatibility = options.controls?.checkCompatibility ?? checkSchemaConsumerCompatibility;
|
|
521
|
+
const rename = options.controls?.rename ?? renameSync;
|
|
522
|
+
let outcome;
|
|
523
|
+
const evaluateCandidate = () => {
|
|
524
|
+
try {
|
|
525
|
+
writeFileSync(tempPath, options.schema, "utf8");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return makeResult("schema_candidate_write_failed", initial, {
|
|
528
|
+
message: messageFrom(error)
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
let validation;
|
|
532
|
+
try {
|
|
533
|
+
validation = validate({
|
|
534
|
+
input: tempPath,
|
|
535
|
+
project: options.project,
|
|
536
|
+
cwd
|
|
537
|
+
});
|
|
538
|
+
} catch (error) {
|
|
539
|
+
return makeResult("schema_candidate_validation_unavailable", initial, {
|
|
540
|
+
message: messageFrom(error)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if (!validation.ok) {
|
|
544
|
+
const status = validation.status === "invalid_artifact" ? "schema_candidate_invalid" : "schema_candidate_validation_unavailable";
|
|
545
|
+
return makeResult(status, initial, {
|
|
546
|
+
message: validationFailureMessage(validation)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const oldSource = initial.bytes?.toString("utf8");
|
|
550
|
+
if (oldSource !== void 0 && hasGeneratedDatabaseObjects(oldSource) && !hasGeneratedDatabaseObjects(options.schema)) {
|
|
551
|
+
return makeResult("schema_zero_objects", initial, {
|
|
552
|
+
message: `Refusing to overwrite existing schema with a zero-object candidate: ${options.displayPath}`
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (initial.bytes?.equals(Buffer.from(options.schema, "utf8"))) {
|
|
556
|
+
try {
|
|
557
|
+
options.controls?.beforeFinalTargetRead?.();
|
|
558
|
+
const current2 = readTargetSnapshot(outputPath);
|
|
559
|
+
if (current2.sha !== initial.sha) {
|
|
560
|
+
return makeResult("schema_target_changed", initial, {
|
|
561
|
+
targetShaAfter: current2.sha,
|
|
562
|
+
message: "Schema target changed after validation; the candidate was not installed"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return makeResult("schema_unchanged", initial, {
|
|
566
|
+
ok: true,
|
|
567
|
+
targetShaAfter: current2.sha
|
|
568
|
+
});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
return makeResult("schema_target_read_failed", initial, {
|
|
571
|
+
message: messageFrom(error)
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
let compatibility;
|
|
576
|
+
if (oldSource !== void 0) {
|
|
577
|
+
try {
|
|
578
|
+
compatibility = checkCompatibility({
|
|
579
|
+
cwd,
|
|
580
|
+
targetPath: outputPath,
|
|
581
|
+
oldSource,
|
|
582
|
+
candidateSource: options.schema,
|
|
583
|
+
project: options.project
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
return makeResult("schema_compatibility_unavailable", initial, {
|
|
587
|
+
message: messageFrom(error)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (!compatibility.ok) {
|
|
591
|
+
return compatibilityFailureResult(initial, compatibility);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
let current;
|
|
595
|
+
try {
|
|
596
|
+
options.controls?.beforeFinalTargetRead?.();
|
|
597
|
+
current = readTargetSnapshot(outputPath);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return makeResult("schema_target_read_failed", initial, {
|
|
600
|
+
removedExportCount: compatibility?.removedExportCount,
|
|
601
|
+
message: messageFrom(error)
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (current.sha !== initial.sha) {
|
|
605
|
+
return makeResult("schema_target_changed", initial, {
|
|
606
|
+
targetShaAfter: current.sha,
|
|
607
|
+
removedExportCount: compatibility?.removedExportCount,
|
|
608
|
+
message: "Schema target changed after validation; the candidate was not installed"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
rename(tempPath, outputPath);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return makeResult("schema_replace_failed", initial, {
|
|
615
|
+
removedExportCount: compatibility?.removedExportCount,
|
|
616
|
+
message: messageFrom(error)
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return makeResult("schema_written", initial, {
|
|
620
|
+
ok: true,
|
|
621
|
+
changed: true,
|
|
622
|
+
targetShaAfter: sha256(Buffer.from(options.schema, "utf8")),
|
|
623
|
+
removedExportCount: compatibility?.removedExportCount
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
outcome = evaluateCandidate();
|
|
627
|
+
try {
|
|
628
|
+
if (existsSync(tempPath)) {
|
|
629
|
+
rmSync(tempPath, { force: true });
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
outcome = makeResult("schema_cleanup_failed", initial, {
|
|
633
|
+
removedExportCount: outcome.removedExportCount,
|
|
634
|
+
newDiagnosticCount: outcome.newDiagnosticCount,
|
|
635
|
+
diagnosticSummary: outcome.diagnosticSummary,
|
|
636
|
+
message: `Temporary candidate cleanup failed: ${messageFrom(error)}`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return outcome;
|
|
640
|
+
}
|
|
641
|
+
|
|
431
642
|
// src/bin.ts
|
|
432
643
|
function getValidateExitCode(result) {
|
|
433
644
|
if (result.ok) return 0;
|
|
@@ -435,8 +646,38 @@ function getValidateExitCode(result) {
|
|
|
435
646
|
if (result.status === "inconclusive") return 3;
|
|
436
647
|
return 1;
|
|
437
648
|
}
|
|
438
|
-
|
|
439
|
-
const
|
|
649
|
+
function boundedMessage2(error) {
|
|
650
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
651
|
+
return message.length > 500 ? `${message.slice(0, 500)}...` : message;
|
|
652
|
+
}
|
|
653
|
+
function generationFailure(error) {
|
|
654
|
+
return {
|
|
655
|
+
contract: SCHEMA_SYNC_CONTRACT,
|
|
656
|
+
ok: false,
|
|
657
|
+
status: "schema_generation_failed",
|
|
658
|
+
changed: false,
|
|
659
|
+
targetShaBefore: null,
|
|
660
|
+
targetShaAfter: null,
|
|
661
|
+
removedExportCount: 0,
|
|
662
|
+
newDiagnosticCount: 0,
|
|
663
|
+
diagnosticSummary: [],
|
|
664
|
+
message: boundedMessage2(error)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function emitSchemaSyncResult(result) {
|
|
668
|
+
console.log(JSON.stringify(result));
|
|
669
|
+
}
|
|
670
|
+
function isDirectExecution() {
|
|
671
|
+
const entryPath = process.argv[1];
|
|
672
|
+
if (!entryPath) return false;
|
|
673
|
+
try {
|
|
674
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve2(entryPath));
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function main(argv = process.argv) {
|
|
680
|
+
const cliArgs = parseArgs(argv);
|
|
440
681
|
if (cliArgs.command === "validate") {
|
|
441
682
|
if (!cliArgs.input) {
|
|
442
683
|
const output3 = {
|
|
@@ -447,18 +688,20 @@ async function main() {
|
|
|
447
688
|
message: "--input is required for validate command"
|
|
448
689
|
};
|
|
449
690
|
console.log(JSON.stringify(output3));
|
|
450
|
-
process.
|
|
691
|
+
process.exitCode = 1;
|
|
692
|
+
return;
|
|
451
693
|
}
|
|
452
694
|
const output2 = validateGeneratedSchema({
|
|
453
695
|
input: cliArgs.input,
|
|
454
696
|
project: cliArgs.project
|
|
455
697
|
});
|
|
456
698
|
console.log(JSON.stringify(output2));
|
|
457
|
-
process.
|
|
699
|
+
process.exitCode = getValidateExitCode(output2);
|
|
700
|
+
return;
|
|
458
701
|
}
|
|
459
702
|
for (const name of [".env.local", ".env"]) {
|
|
460
|
-
const envPath =
|
|
461
|
-
if (
|
|
703
|
+
const envPath = resolve2(process.cwd(), name);
|
|
704
|
+
if (existsSync2(envPath)) {
|
|
462
705
|
(0, import_dotenv.config)({ path: envPath });
|
|
463
706
|
console.log(`[db-schema-sync] \u2713 Loaded ${name}`);
|
|
464
707
|
}
|
|
@@ -474,21 +717,45 @@ async function main() {
|
|
|
474
717
|
...options,
|
|
475
718
|
exportCustomTypes: cliArgs.exportCustomTypes
|
|
476
719
|
});
|
|
477
|
-
if (!
|
|
720
|
+
if (!hasGeneratedDatabaseObjects(schema)) {
|
|
478
721
|
console.warn(
|
|
479
722
|
"[db-schema-sync] \u26A0 Warning: No tables or views found in database. The generated schema will be empty."
|
|
480
723
|
);
|
|
481
724
|
}
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
725
|
+
const result = writeSchemaFileSafely({
|
|
726
|
+
outputPath: resolve2(process.cwd(), output),
|
|
727
|
+
displayPath: output,
|
|
728
|
+
schema,
|
|
729
|
+
project: cliArgs.project,
|
|
730
|
+
cwd: process.cwd()
|
|
731
|
+
});
|
|
732
|
+
if (!result.ok) {
|
|
733
|
+
console.error(
|
|
734
|
+
`[db-schema-sync] \u2717 Failed (${result.status}): ${result.message ?? "schema was not replaced"}`
|
|
735
|
+
);
|
|
736
|
+
emitSchemaSyncResult(result);
|
|
737
|
+
process.exitCode = 1;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (result.changed) {
|
|
741
|
+
console.log(`[db-schema-sync] \u2713 Schema written to ${output}`);
|
|
742
|
+
} else {
|
|
743
|
+
console.log(`[db-schema-sync] \u2713 Schema unchanged at ${output}`);
|
|
744
|
+
}
|
|
745
|
+
emitSchemaSyncResult(result);
|
|
746
|
+
process.exitCode = 0;
|
|
486
747
|
} catch (err) {
|
|
748
|
+
const result = generationFailure(err);
|
|
487
749
|
console.error(
|
|
488
|
-
`[db-schema-sync] \u2717 Failed
|
|
489
|
-
err instanceof Error ? err.message : String(err)
|
|
750
|
+
`[db-schema-sync] \u2717 Failed (${result.status}): ${result.message}`
|
|
490
751
|
);
|
|
491
|
-
|
|
752
|
+
emitSchemaSyncResult(result);
|
|
753
|
+
process.exitCode = 1;
|
|
492
754
|
}
|
|
493
755
|
}
|
|
494
|
-
|
|
756
|
+
if (isDirectExecution()) {
|
|
757
|
+
void main();
|
|
758
|
+
}
|
|
759
|
+
export {
|
|
760
|
+
main
|
|
761
|
+
};
|
|
@@ -2673,10 +2673,15 @@ import {
|
|
|
2673
2673
|
rmSync,
|
|
2674
2674
|
writeFileSync
|
|
2675
2675
|
} from "fs";
|
|
2676
|
+
import { createRequire as createNodeRequire } from "module";
|
|
2676
2677
|
import { tmpdir } from "os";
|
|
2677
|
-
import { dirname, delimiter, join, resolve } from "path";
|
|
2678
|
+
import { dirname, delimiter, join, relative, resolve } from "path";
|
|
2679
|
+
var MAX_MESSAGE_LENGTH = 500;
|
|
2680
|
+
var MAX_DIAGNOSTIC_MESSAGE_LENGTH = 300;
|
|
2681
|
+
var MAX_DIAGNOSTIC_FILE_LENGTH = 160;
|
|
2682
|
+
var MAX_DIAGNOSTIC_SUMMARY_COUNT = 5;
|
|
2678
2683
|
function truncateMessage(message) {
|
|
2679
|
-
return message.length >
|
|
2684
|
+
return message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, MAX_MESSAGE_LENGTH)}...` : message;
|
|
2680
2685
|
}
|
|
2681
2686
|
function result(params, startTime) {
|
|
2682
2687
|
return {
|
|
@@ -2917,6 +2922,320 @@ function validateGeneratedSchema(options) {
|
|
|
2917
2922
|
);
|
|
2918
2923
|
}
|
|
2919
2924
|
}
|
|
2925
|
+
function compatibilityUnavailable(message, removedExportCount = 0) {
|
|
2926
|
+
return {
|
|
2927
|
+
ok: false,
|
|
2928
|
+
status: "unavailable",
|
|
2929
|
+
removedExportCount,
|
|
2930
|
+
oldDiagnosticCount: 0,
|
|
2931
|
+
candidateDiagnosticCount: 0,
|
|
2932
|
+
newDiagnosticCount: 0,
|
|
2933
|
+
diagnosticSummary: [],
|
|
2934
|
+
message: truncateMessage(message)
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
function loadProjectTypeScript(cwd) {
|
|
2938
|
+
const projectRequire = createNodeRequire(join(cwd, "package.json"));
|
|
2939
|
+
return projectRequire("typescript");
|
|
2940
|
+
}
|
|
2941
|
+
function resolveConsumerProjectPath(cwd, project) {
|
|
2942
|
+
if (project) return resolve(cwd, project);
|
|
2943
|
+
const projectPath = resolve(cwd, "tsconfig.node.json");
|
|
2944
|
+
return existsSync(projectPath) ? projectPath : void 0;
|
|
2945
|
+
}
|
|
2946
|
+
function canonicalPath(path) {
|
|
2947
|
+
const absolute = resolve(path).replace(/\\/g, "/");
|
|
2948
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
2949
|
+
}
|
|
2950
|
+
function hasExportModifier(ts, node) {
|
|
2951
|
+
const modifiers = node.modifiers;
|
|
2952
|
+
return Boolean(
|
|
2953
|
+
modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
function collectBindingNames(ts, name, names) {
|
|
2957
|
+
if (ts.isIdentifier(name)) {
|
|
2958
|
+
names.add(name.text);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
for (const element of name.elements) {
|
|
2962
|
+
if (!ts.isOmittedExpression(element)) {
|
|
2963
|
+
collectBindingNames(ts, element.name, names);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
function collectPublicExportNames(ts, source) {
|
|
2968
|
+
const sourceFile = ts.createSourceFile(
|
|
2969
|
+
"schema.ts",
|
|
2970
|
+
source,
|
|
2971
|
+
ts.ScriptTarget.Latest,
|
|
2972
|
+
false,
|
|
2973
|
+
ts.ScriptKind.TS
|
|
2974
|
+
);
|
|
2975
|
+
const names = /* @__PURE__ */ new Set();
|
|
2976
|
+
let complete = true;
|
|
2977
|
+
for (const statement of sourceFile.statements) {
|
|
2978
|
+
if (ts.isExportDeclaration(statement)) {
|
|
2979
|
+
if (!statement.exportClause) {
|
|
2980
|
+
complete = false;
|
|
2981
|
+
} else if (ts.isNamedExports(statement.exportClause)) {
|
|
2982
|
+
for (const element of statement.exportClause.elements) {
|
|
2983
|
+
names.add(element.name.text);
|
|
2984
|
+
}
|
|
2985
|
+
} else {
|
|
2986
|
+
names.add(statement.exportClause.name.text);
|
|
2987
|
+
}
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
if (ts.isExportAssignment(statement)) {
|
|
2991
|
+
names.add("default");
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
if (!hasExportModifier(ts, statement)) continue;
|
|
2995
|
+
if (ts.isVariableStatement(statement)) {
|
|
2996
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2997
|
+
collectBindingNames(ts, declaration.name, names);
|
|
2998
|
+
}
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) {
|
|
3002
|
+
if (statement.name) {
|
|
3003
|
+
names.add(statement.name.text);
|
|
3004
|
+
} else {
|
|
3005
|
+
names.add("default");
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return { names, complete };
|
|
3010
|
+
}
|
|
3011
|
+
function fallbackRemovedExportCount(oldSource, candidateSource) {
|
|
3012
|
+
const declarationPattern = /^\s*export\s+(?:declare\s+)?(?:const|let|var|function|class|interface|type|enum|namespace|module)\s+([A-Za-z_$][\w$]*)/gm;
|
|
3013
|
+
const collect = (source) => {
|
|
3014
|
+
const names = /* @__PURE__ */ new Set();
|
|
3015
|
+
for (const match of source.matchAll(declarationPattern)) {
|
|
3016
|
+
names.add(match[1]);
|
|
3017
|
+
}
|
|
3018
|
+
return names;
|
|
3019
|
+
};
|
|
3020
|
+
const oldNames = collect(oldSource);
|
|
3021
|
+
const candidateNames = collect(candidateSource);
|
|
3022
|
+
return [...oldNames].filter((name) => !candidateNames.has(name)).length;
|
|
3023
|
+
}
|
|
3024
|
+
function createOverlayProgram(params) {
|
|
3025
|
+
const target = canonicalPath(params.targetPath);
|
|
3026
|
+
const host = params.ts.createCompilerHost(params.parsed.options, true);
|
|
3027
|
+
const defaultFileExists = host.fileExists.bind(host);
|
|
3028
|
+
const defaultReadFile = host.readFile.bind(host);
|
|
3029
|
+
const defaultGetSourceFile = host.getSourceFile.bind(host);
|
|
3030
|
+
const isTarget = (fileName) => canonicalPath(fileName) === target;
|
|
3031
|
+
host.fileExists = (fileName) => isTarget(fileName) ? true : defaultFileExists(fileName);
|
|
3032
|
+
host.readFile = (fileName) => isTarget(fileName) ? params.source : defaultReadFile(fileName);
|
|
3033
|
+
host.getSourceFile = (fileName, languageVersion, onError, shouldCreate) => {
|
|
3034
|
+
if (isTarget(fileName)) {
|
|
3035
|
+
return params.ts.createSourceFile(
|
|
3036
|
+
fileName,
|
|
3037
|
+
params.source,
|
|
3038
|
+
languageVersion,
|
|
3039
|
+
true,
|
|
3040
|
+
params.ts.ScriptKind.TS
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
return defaultGetSourceFile(
|
|
3044
|
+
fileName,
|
|
3045
|
+
languageVersion,
|
|
3046
|
+
onError,
|
|
3047
|
+
shouldCreate
|
|
3048
|
+
);
|
|
3049
|
+
};
|
|
3050
|
+
return params.ts.createProgram({
|
|
3051
|
+
rootNames: params.parsed.fileNames,
|
|
3052
|
+
options: params.parsed.options,
|
|
3053
|
+
projectReferences: params.parsed.projectReferences,
|
|
3054
|
+
host
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
function isServerDiagnostic(diagnostic, cwd) {
|
|
3058
|
+
if (!diagnostic.file) return true;
|
|
3059
|
+
const path = relative(cwd, resolve(diagnostic.file.fileName)).replace(
|
|
3060
|
+
/\\/g,
|
|
3061
|
+
"/"
|
|
3062
|
+
);
|
|
3063
|
+
return path === "server" || path.startsWith("server/");
|
|
3064
|
+
}
|
|
3065
|
+
function normalizeDiagnostic(ts, diagnostic, cwd) {
|
|
3066
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, " ");
|
|
3067
|
+
const file = diagnostic.file ? relative(cwd, resolve(diagnostic.file.fileName)).replace(/\\/g, "/") : void 0;
|
|
3068
|
+
const position = diagnostic.file && diagnostic.start !== void 0 ? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start) : void 0;
|
|
3069
|
+
const line = position ? position.line + 1 : void 0;
|
|
3070
|
+
const character = position ? position.character + 1 : void 0;
|
|
3071
|
+
const key = [
|
|
3072
|
+
diagnostic.code,
|
|
3073
|
+
diagnostic.category,
|
|
3074
|
+
file ?? "",
|
|
3075
|
+
line ?? "",
|
|
3076
|
+
character ?? "",
|
|
3077
|
+
message
|
|
3078
|
+
].join("|");
|
|
3079
|
+
return {
|
|
3080
|
+
key,
|
|
3081
|
+
summary: {
|
|
3082
|
+
code: diagnostic.code,
|
|
3083
|
+
file: file && file.length > MAX_DIAGNOSTIC_FILE_LENGTH ? `...${file.slice(-MAX_DIAGNOSTIC_FILE_LENGTH)}` : file,
|
|
3084
|
+
line,
|
|
3085
|
+
character,
|
|
3086
|
+
message: message.length > MAX_DIAGNOSTIC_MESSAGE_LENGTH ? `${message.slice(0, MAX_DIAGNOSTIC_MESSAGE_LENGTH)}...` : message
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
function getServerDiagnostics(ts, program, cwd) {
|
|
3091
|
+
return ts.getPreEmitDiagnostics(program).filter((diagnostic) => isServerDiagnostic(diagnostic, cwd));
|
|
3092
|
+
}
|
|
3093
|
+
function getNewDiagnostics(params) {
|
|
3094
|
+
const oldCounts = /* @__PURE__ */ new Map();
|
|
3095
|
+
for (const diagnostic of params.oldDiagnostics) {
|
|
3096
|
+
const { key } = normalizeDiagnostic(params.ts, diagnostic, params.cwd);
|
|
3097
|
+
oldCounts.set(key, (oldCounts.get(key) ?? 0) + 1);
|
|
3098
|
+
}
|
|
3099
|
+
const additions = [];
|
|
3100
|
+
for (const diagnostic of params.candidateDiagnostics) {
|
|
3101
|
+
const normalized = normalizeDiagnostic(params.ts, diagnostic, params.cwd);
|
|
3102
|
+
const oldCount = oldCounts.get(normalized.key) ?? 0;
|
|
3103
|
+
if (oldCount > 0) {
|
|
3104
|
+
oldCounts.set(normalized.key, oldCount - 1);
|
|
3105
|
+
} else {
|
|
3106
|
+
additions.push(normalized);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
additions.sort((left, right) => left.key.localeCompare(right.key));
|
|
3110
|
+
return additions.map((addition) => addition.summary);
|
|
3111
|
+
}
|
|
3112
|
+
function checkSchemaConsumerCompatibility(options) {
|
|
3113
|
+
const cwd = resolve(options.cwd);
|
|
3114
|
+
const targetPath = resolve(cwd, options.targetPath);
|
|
3115
|
+
let ts;
|
|
3116
|
+
try {
|
|
3117
|
+
ts = options.typescript ?? loadProjectTypeScript(cwd);
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
return compatibilityUnavailable(
|
|
3120
|
+
`Project TypeScript compiler is unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
3121
|
+
fallbackRemovedExportCount(options.oldSource, options.candidateSource)
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
let removedExportCount = 0;
|
|
3125
|
+
try {
|
|
3126
|
+
const oldExports = collectPublicExportNames(ts, options.oldSource);
|
|
3127
|
+
const candidateExports = collectPublicExportNames(
|
|
3128
|
+
ts,
|
|
3129
|
+
options.candidateSource
|
|
3130
|
+
);
|
|
3131
|
+
removedExportCount = [...oldExports.names].filter(
|
|
3132
|
+
(name) => !candidateExports.names.has(name)
|
|
3133
|
+
).length;
|
|
3134
|
+
if (!oldExports.complete || !candidateExports.complete) {
|
|
3135
|
+
return compatibilityUnavailable(
|
|
3136
|
+
"Schema public exports could not be completely enumerated",
|
|
3137
|
+
removedExportCount
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
return compatibilityUnavailable(
|
|
3142
|
+
`Schema public exports could not be compared: ${error instanceof Error ? error.message : String(error)}`,
|
|
3143
|
+
fallbackRemovedExportCount(options.oldSource, options.candidateSource)
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
const projectPath = resolveConsumerProjectPath(cwd, options.project);
|
|
3147
|
+
if (!projectPath || !existsSync(projectPath)) {
|
|
3148
|
+
return compatibilityUnavailable(
|
|
3149
|
+
"tsconfig.node.json is required for schema consumer compatibility checking",
|
|
3150
|
+
removedExportCount
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
try {
|
|
3154
|
+
const config = ts.readConfigFile(projectPath, ts.sys.readFile);
|
|
3155
|
+
if (config.error) {
|
|
3156
|
+
const message = ts.flattenDiagnosticMessageText(
|
|
3157
|
+
config.error.messageText,
|
|
3158
|
+
" "
|
|
3159
|
+
);
|
|
3160
|
+
return compatibilityUnavailable(
|
|
3161
|
+
`Unable to read TypeScript project configuration: ${message}`,
|
|
3162
|
+
removedExportCount
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
3166
|
+
config.config,
|
|
3167
|
+
ts.sys,
|
|
3168
|
+
dirname(projectPath),
|
|
3169
|
+
{
|
|
3170
|
+
noEmit: true,
|
|
3171
|
+
incremental: false,
|
|
3172
|
+
composite: false
|
|
3173
|
+
},
|
|
3174
|
+
projectPath
|
|
3175
|
+
);
|
|
3176
|
+
if (parsed.errors.length > 0) {
|
|
3177
|
+
const message = parsed.errors.slice(0, MAX_DIAGNOSTIC_SUMMARY_COUNT).map((error) => ts.flattenDiagnosticMessageText(error.messageText, " ")).join("; ");
|
|
3178
|
+
return compatibilityUnavailable(
|
|
3179
|
+
`Unable to parse TypeScript project configuration: ${message}`,
|
|
3180
|
+
removedExportCount
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
const oldProgram = createOverlayProgram({
|
|
3184
|
+
ts,
|
|
3185
|
+
parsed,
|
|
3186
|
+
targetPath,
|
|
3187
|
+
source: options.oldSource
|
|
3188
|
+
});
|
|
3189
|
+
const candidateProgram = createOverlayProgram({
|
|
3190
|
+
ts,
|
|
3191
|
+
parsed,
|
|
3192
|
+
targetPath,
|
|
3193
|
+
source: options.candidateSource
|
|
3194
|
+
});
|
|
3195
|
+
const oldDiagnostics = getServerDiagnostics(ts, oldProgram, cwd);
|
|
3196
|
+
const candidateDiagnostics = getServerDiagnostics(
|
|
3197
|
+
ts,
|
|
3198
|
+
candidateProgram,
|
|
3199
|
+
cwd
|
|
3200
|
+
);
|
|
3201
|
+
const newDiagnostics = getNewDiagnostics({
|
|
3202
|
+
ts,
|
|
3203
|
+
cwd,
|
|
3204
|
+
oldDiagnostics,
|
|
3205
|
+
candidateDiagnostics
|
|
3206
|
+
});
|
|
3207
|
+
const diagnosticSummary = newDiagnostics.slice(
|
|
3208
|
+
0,
|
|
3209
|
+
MAX_DIAGNOSTIC_SUMMARY_COUNT
|
|
3210
|
+
);
|
|
3211
|
+
if (newDiagnostics.length > 0) {
|
|
3212
|
+
return {
|
|
3213
|
+
ok: false,
|
|
3214
|
+
status: "incompatible",
|
|
3215
|
+
removedExportCount,
|
|
3216
|
+
oldDiagnosticCount: oldDiagnostics.length,
|
|
3217
|
+
candidateDiagnosticCount: candidateDiagnostics.length,
|
|
3218
|
+
newDiagnosticCount: newDiagnostics.length,
|
|
3219
|
+
diagnosticSummary,
|
|
3220
|
+
message: "Candidate schema introduces new server TypeScript diagnostics"
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
return {
|
|
3224
|
+
ok: true,
|
|
3225
|
+
status: "compatible",
|
|
3226
|
+
removedExportCount,
|
|
3227
|
+
oldDiagnosticCount: oldDiagnostics.length,
|
|
3228
|
+
candidateDiagnosticCount: candidateDiagnostics.length,
|
|
3229
|
+
newDiagnosticCount: 0,
|
|
3230
|
+
diagnosticSummary: []
|
|
3231
|
+
};
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
return compatibilityUnavailable(
|
|
3234
|
+
`Schema consumer compatibility check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3235
|
+
removedExportCount
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
2920
3239
|
|
|
2921
3240
|
// src/index.ts
|
|
2922
3241
|
async function generateSchema(options) {
|
|
@@ -2942,6 +3261,7 @@ export {
|
|
|
2942
3261
|
normalizeApiResponse,
|
|
2943
3262
|
generateSchemaCode,
|
|
2944
3263
|
validateGeneratedSchema,
|
|
3264
|
+
checkSchemaConsumerCompatibility,
|
|
2945
3265
|
generateSchema,
|
|
2946
3266
|
generateSchemaFromData
|
|
2947
3267
|
};
|
package/dist/index.js
CHANGED