@jskit-ai/jskit-catalog 0.2.9 → 0.2.11
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/catalog/packages.json +492 -272
- package/package.json +1 -1
- package/scripts/build-catalog.mjs +1 -1
- package/scripts/files.mjs +17 -0
- package/scripts/framework-packages.mjs +2 -10
- package/scripts/i18n.mjs +2 -1
- package/scripts/pattern-assets.mjs +4 -7
- package/scripts/project-install.mjs +156 -0
- package/scripts/project-manifests.mjs +133 -0
- package/scripts/project-packages.mjs +14 -298
- package/scripts/verify-packages.mjs +69 -38
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
access,
|
|
4
|
-
mkdtemp,
|
|
5
|
-
readFile,
|
|
6
|
-
readdir,
|
|
7
|
-
realpath,
|
|
8
|
-
rename,
|
|
9
|
-
rm,
|
|
10
|
-
writeFile
|
|
11
|
-
} from "node:fs/promises";
|
|
1
|
+
import { realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
12
2
|
import path from "node:path";
|
|
3
|
+
import { fileExists, toPosixPath } from "./files.mjs";
|
|
4
|
+
import { readOptionalFile, restoreProjectFiles, runNpmInstall, writeManifestUpdates } from "./project-install.mjs";
|
|
5
|
+
import { discoverProjectManifests, readJsonRecord, serializePackageJson } from "./project-manifests.mjs";
|
|
13
6
|
|
|
14
7
|
const JSKIT_SCOPE_PREFIX = "@jskit-ai/";
|
|
15
8
|
const CATALOG_PACKAGE_ID = "@jskit-ai/jskit-catalog";
|
|
@@ -19,142 +12,9 @@ const DEPENDENCY_FIELDS = Object.freeze([
|
|
|
19
12
|
"peerDependencies",
|
|
20
13
|
"optionalDependencies"
|
|
21
14
|
]);
|
|
22
|
-
const SKIPPED_DIRECTORY_NAMES = new Set([
|
|
23
|
-
".git",
|
|
24
|
-
".cache",
|
|
25
|
-
".next",
|
|
26
|
-
".nuxt",
|
|
27
|
-
".output",
|
|
28
|
-
".turbo",
|
|
29
|
-
"build",
|
|
30
|
-
"coverage",
|
|
31
|
-
"dist",
|
|
32
|
-
"node_modules",
|
|
33
|
-
"tmp"
|
|
34
|
-
]);
|
|
35
15
|
const UPDATE_SCRIPT = "npx --yes @jskit-ai/jskit-catalog@latest update";
|
|
36
16
|
const CHECK_SCRIPT = "jskit check";
|
|
37
17
|
|
|
38
|
-
function toPosixPath(value = "") {
|
|
39
|
-
return String(value || "").split(path.sep).join("/");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeWorkspacePatterns(value) {
|
|
43
|
-
if (Array.isArray(value)) {
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
if (value && typeof value === "object" && Array.isArray(value.packages)) {
|
|
47
|
-
return value.packages;
|
|
48
|
-
}
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function globToRegExp(pattern = "") {
|
|
53
|
-
const source = toPosixPath(pattern).replace(/^\.\//u, "").replace(/\/$/u, "");
|
|
54
|
-
let expression = "^";
|
|
55
|
-
|
|
56
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
57
|
-
const character = source[index];
|
|
58
|
-
if (character === "*") {
|
|
59
|
-
if (source[index + 1] === "*") {
|
|
60
|
-
index += 1;
|
|
61
|
-
if (source[index + 1] === "/") {
|
|
62
|
-
index += 1;
|
|
63
|
-
expression += "(?:.*/)?";
|
|
64
|
-
} else {
|
|
65
|
-
expression += ".*";
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
expression += "[^/]*";
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (character === "?") {
|
|
73
|
-
expression += "[^/]";
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
expression += /[.+^${}()|[\]\\]/u.test(character) ? `\\${character}` : character;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return new RegExp(`${expression}$`, "u");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function createWorkspaceMatcher(workspaces) {
|
|
83
|
-
const patterns = normalizeWorkspacePatterns(workspaces)
|
|
84
|
-
.map((entry) => String(entry || "").trim())
|
|
85
|
-
.filter(Boolean);
|
|
86
|
-
const includes = patterns
|
|
87
|
-
.filter((entry) => !entry.startsWith("!"))
|
|
88
|
-
.map(globToRegExp);
|
|
89
|
-
const excludes = patterns
|
|
90
|
-
.filter((entry) => entry.startsWith("!"))
|
|
91
|
-
.map((entry) => globToRegExp(entry.slice(1)));
|
|
92
|
-
|
|
93
|
-
return function matchesWorkspace(relativeDirectory = "") {
|
|
94
|
-
const normalized = toPosixPath(relativeDirectory).replace(/^\.\//u, "").replace(/\/$/u, "");
|
|
95
|
-
return includes.some((matcher) => matcher.test(normalized)) &&
|
|
96
|
-
!excludes.some((matcher) => matcher.test(normalized));
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function fileExists(absolutePath) {
|
|
101
|
-
try {
|
|
102
|
-
await access(absolutePath);
|
|
103
|
-
return true;
|
|
104
|
-
} catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function readJsonRecord(absolutePath) {
|
|
110
|
-
const contents = await readFile(absolutePath, "utf8");
|
|
111
|
-
let value;
|
|
112
|
-
try {
|
|
113
|
-
value = JSON.parse(contents);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
throw new Error(`${absolutePath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
|
-
}
|
|
117
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
118
|
-
throw new TypeError(`${absolutePath} must contain a JSON object.`);
|
|
119
|
-
}
|
|
120
|
-
return { absolutePath, contents, value };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function collectNestedPackageJsonPaths(directory, results = []) {
|
|
124
|
-
for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
|
|
125
|
-
if (!entry.isDirectory() || SKIPPED_DIRECTORY_NAMES.has(entry.name) || entry.name.startsWith(".")) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const childDirectory = path.join(directory, entry.name);
|
|
129
|
-
const manifestPath = path.join(childDirectory, "package.json");
|
|
130
|
-
if (await fileExists(manifestPath)) {
|
|
131
|
-
results.push(manifestPath);
|
|
132
|
-
}
|
|
133
|
-
await collectNestedPackageJsonPaths(childDirectory, results);
|
|
134
|
-
}
|
|
135
|
-
return results;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function discoverProjectManifests(projectRoot, additionalManifests = []) {
|
|
139
|
-
const root = path.resolve(projectRoot);
|
|
140
|
-
const rootManifestPath = path.join(root, "package.json");
|
|
141
|
-
if (!(await fileExists(rootManifestPath))) {
|
|
142
|
-
throw new Error(`JSKIT project root has no package.json: ${root}`);
|
|
143
|
-
}
|
|
144
|
-
const rootManifest = await readJsonRecord(rootManifestPath);
|
|
145
|
-
const matchesWorkspace = createWorkspaceMatcher(rootManifest.value.workspaces);
|
|
146
|
-
const nestedPaths = await collectNestedPackageJsonPaths(root);
|
|
147
|
-
const workspacePaths = nestedPaths
|
|
148
|
-
.filter((manifestPath) => matchesWorkspace(path.relative(root, path.dirname(manifestPath))))
|
|
149
|
-
.sort((left, right) => left.localeCompare(right));
|
|
150
|
-
const manifests = [rootManifest];
|
|
151
|
-
for (const manifestPath of new Set([...workspacePaths, ...additionalManifests.map((file) => path.resolve(root, file))])) {
|
|
152
|
-
if (manifestPath === rootManifestPath) continue;
|
|
153
|
-
manifests.push(await readJsonRecord(manifestPath));
|
|
154
|
-
}
|
|
155
|
-
return Object.freeze(manifests);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
18
|
function normalizeCatalog(catalog = {}) {
|
|
159
19
|
const releasePackages = catalog?.release?.packages;
|
|
160
20
|
if (!releasePackages || typeof releasePackages !== "object" || Array.isArray(releasePackages)) {
|
|
@@ -177,12 +37,6 @@ function normalizeCatalog(catalog = {}) {
|
|
|
177
37
|
return Object.freeze({ versions, singletonPackages });
|
|
178
38
|
}
|
|
179
39
|
|
|
180
|
-
function serializePackageJson(value, originalContents = "") {
|
|
181
|
-
const indentMatch = /\n([ \t]+)"/u.exec(String(originalContents || ""));
|
|
182
|
-
const indentation = indentMatch?.[1] || " ";
|
|
183
|
-
return `${JSON.stringify(value, null, indentation)}\n`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
40
|
function collectManifestDeclarations(manifests, projectRoot) {
|
|
187
41
|
const declarations = [];
|
|
188
42
|
for (const manifest of manifests) {
|
|
@@ -464,68 +318,6 @@ async function checkInstalledGraph({ catalog, manifestPaths = [], sourcePackages
|
|
|
464
318
|
return { ok: issues.length === 0, issues, installations };
|
|
465
319
|
}
|
|
466
320
|
|
|
467
|
-
async function writeManifestUpdates(updates = []) {
|
|
468
|
-
const temporaryPaths = [];
|
|
469
|
-
const committed = [];
|
|
470
|
-
try {
|
|
471
|
-
for (const [index, update] of updates.entries()) {
|
|
472
|
-
const temporaryPath = `${update.manifest.absolutePath}.jskit-${process.pid}-${index}.tmp`;
|
|
473
|
-
await writeFile(temporaryPath, update.contents, "utf8");
|
|
474
|
-
temporaryPaths.push(temporaryPath);
|
|
475
|
-
}
|
|
476
|
-
for (const [index, update] of updates.entries()) {
|
|
477
|
-
await rename(temporaryPaths[index], update.manifest.absolutePath);
|
|
478
|
-
committed.push(update);
|
|
479
|
-
}
|
|
480
|
-
} catch (error) {
|
|
481
|
-
for (const update of committed) {
|
|
482
|
-
await writeFile(update.manifest.absolutePath, update.manifest.contents, "utf8");
|
|
483
|
-
}
|
|
484
|
-
throw error;
|
|
485
|
-
} finally {
|
|
486
|
-
await Promise.all(temporaryPaths.map((temporaryPath) => rm(temporaryPath, { force: true })));
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
async function readOptionalFile(absolutePath) {
|
|
491
|
-
try {
|
|
492
|
-
return Object.freeze({
|
|
493
|
-
contents: await readFile(absolutePath, "utf8"),
|
|
494
|
-
exists: true
|
|
495
|
-
});
|
|
496
|
-
} catch (error) {
|
|
497
|
-
if (error?.code === "ENOENT") {
|
|
498
|
-
return Object.freeze({
|
|
499
|
-
contents: "",
|
|
500
|
-
exists: false
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
throw error;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async function restoreProjectFiles(updates, packageLockPath, packageLockSnapshot) {
|
|
508
|
-
const rollbackUpdates = updates.map((update) => Object.freeze({
|
|
509
|
-
contents: update.manifest.contents,
|
|
510
|
-
manifest: Object.freeze({
|
|
511
|
-
...update.manifest,
|
|
512
|
-
contents: update.contents
|
|
513
|
-
})
|
|
514
|
-
}));
|
|
515
|
-
await writeManifestUpdates(rollbackUpdates);
|
|
516
|
-
if (packageLockSnapshot.exists) {
|
|
517
|
-
const temporaryPath = `${packageLockPath}.jskit-${process.pid}-rollback.tmp`;
|
|
518
|
-
try {
|
|
519
|
-
await writeFile(temporaryPath, packageLockSnapshot.contents, "utf8");
|
|
520
|
-
await rename(temporaryPath, packageLockPath);
|
|
521
|
-
} finally {
|
|
522
|
-
await rm(temporaryPath, { force: true });
|
|
523
|
-
}
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
await rm(packageLockPath, { force: true });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
321
|
function removeJskitDependencyRecords(dependencies = {}, localPackageNames = new Set()) {
|
|
530
322
|
for (const [packageId, record] of Object.entries(dependencies || {})) {
|
|
531
323
|
if (packageId.startsWith(JSKIT_SCOPE_PREFIX) && !localPackageNames.has(packageId)) {
|
|
@@ -595,90 +387,12 @@ async function preparePackageLockForInstall(projectRoot, manifests, packageLockP
|
|
|
595
387
|
}
|
|
596
388
|
}
|
|
597
389
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
605
|
-
child.on("error", reject);
|
|
606
|
-
child.on("close", (code, signal) => {
|
|
607
|
-
if (code === 0) {
|
|
608
|
-
resolve();
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
reject(new Error(`npm install failed (code=${code}, signal=${signal || "none"}).`));
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
async function runNpmInstall(projectRoot) {
|
|
617
|
-
const nodeModulesPath = path.join(projectRoot, "node_modules");
|
|
618
|
-
const hadNodeModules = await fileExists(nodeModulesPath);
|
|
619
|
-
const backupRoot = hadNodeModules
|
|
620
|
-
? await mkdtemp(path.join(projectRoot, ".jskit-update-"))
|
|
621
|
-
: null;
|
|
622
|
-
// Preserve the dependency directory name so development watchers keep excluding it.
|
|
623
|
-
const backupPath = backupRoot ? path.join(backupRoot, "node_modules") : null;
|
|
624
|
-
if (hadNodeModules) {
|
|
625
|
-
try {
|
|
626
|
-
await rename(nodeModulesPath, backupPath);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
await rm(backupRoot, { force: true, recursive: true });
|
|
629
|
-
throw error;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
let settled = false;
|
|
634
|
-
const transaction = Object.freeze({
|
|
635
|
-
async commit() {
|
|
636
|
-
if (settled) {
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (backupRoot) {
|
|
640
|
-
await rm(backupRoot, { force: true, recursive: true });
|
|
641
|
-
}
|
|
642
|
-
settled = true;
|
|
643
|
-
},
|
|
644
|
-
async rollback() {
|
|
645
|
-
if (settled) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
try {
|
|
649
|
-
await rm(nodeModulesPath, { force: true, recursive: true });
|
|
650
|
-
if (backupPath) {
|
|
651
|
-
await rename(backupPath, nodeModulesPath);
|
|
652
|
-
}
|
|
653
|
-
} catch (error) {
|
|
654
|
-
if (backupPath) {
|
|
655
|
-
throw new Error(`Could not restore node_modules; backup retained at ${backupPath}.`, { cause: error });
|
|
656
|
-
}
|
|
657
|
-
throw error;
|
|
658
|
-
}
|
|
659
|
-
if (backupRoot) {
|
|
660
|
-
await rm(backupRoot, { force: true, recursive: true });
|
|
661
|
-
}
|
|
662
|
-
settled = true;
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
await spawnNpmInstall(projectRoot);
|
|
668
|
-
return transaction;
|
|
669
|
-
} catch (error) {
|
|
670
|
-
try {
|
|
671
|
-
await transaction.rollback();
|
|
672
|
-
} catch (rollbackError) {
|
|
673
|
-
throw new AggregateError(
|
|
674
|
-
[error, rollbackError],
|
|
675
|
-
`npm install failed and the previous node_modules directory could not be restored. ${rollbackError.message}`
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
throw error;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Pins a project's JSKIT dependencies to the catalog and installs them; a
|
|
392
|
+
* failed install or check restores the manifests, lockfile and node_modules.
|
|
393
|
+
* @param {object} options
|
|
394
|
+
* @param {typeof runNpmInstall} [options.installProject] installs the project and returns the transaction that commits or rolls back its node_modules
|
|
395
|
+
*/
|
|
682
396
|
async function updateProject({
|
|
683
397
|
projectRoot = process.cwd(),
|
|
684
398
|
catalog,
|
|
@@ -775,12 +489,14 @@ async function updateProject({
|
|
|
775
489
|
if (!result.ok) {
|
|
776
490
|
throw new Error(`JSKIT graph remains inconsistent after npm install:\n- ${result.issues.join("\n- ")}`);
|
|
777
491
|
}
|
|
778
|
-
await installTransaction
|
|
492
|
+
await installTransaction.commit();
|
|
779
493
|
}
|
|
780
494
|
} catch (error) {
|
|
781
495
|
const rollbackErrors = [];
|
|
782
496
|
try {
|
|
783
|
-
|
|
497
|
+
if (installTransaction) {
|
|
498
|
+
await installTransaction.rollback();
|
|
499
|
+
}
|
|
784
500
|
} catch (rollbackError) {
|
|
785
501
|
rollbackErrors.push(rollbackError);
|
|
786
502
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { fileExists } from "./files.mjs";
|
|
5
6
|
import {
|
|
6
7
|
assertUniquePatternIds,
|
|
7
8
|
discoverPackagePatterns
|
|
@@ -49,15 +50,6 @@ const FORBIDDEN_AUTHORING_FIELDS = new Set([
|
|
|
49
50
|
"options"
|
|
50
51
|
]);
|
|
51
52
|
|
|
52
|
-
async function fileExists(filePath) {
|
|
53
|
-
try {
|
|
54
|
-
await access(filePath);
|
|
55
|
-
return true;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
53
|
function requireStringArray(value, label) {
|
|
62
54
|
if (!Array.isArray(value) || value.some((item) => !String(item || "").trim())) {
|
|
63
55
|
throw new Error(`${label} must be an array of non-empty strings.`);
|
|
@@ -102,10 +94,12 @@ function validateProviderDefinition(value, label) {
|
|
|
102
94
|
validateCapabilityMap(value.requires, `${label}.requires`);
|
|
103
95
|
validateCapabilityMap(value.optional, `${label}.optional`);
|
|
104
96
|
validateCapabilityMap(value.provides, `${label}.provides`);
|
|
97
|
+
// readable-check: allow method-probe - input validation: a package's provider export is checked before the catalog lists it
|
|
105
98
|
if (typeof value.setup !== "function") {
|
|
106
99
|
throw new Error(`${label}.setup must be a function.`);
|
|
107
100
|
}
|
|
108
101
|
for (const lifecycleMethod of ["boot", "shutdown"]) {
|
|
102
|
+
// readable-check: allow method-probe - input validation: a package's provider export is checked before the catalog lists it
|
|
109
103
|
if (value[lifecycleMethod] != null && typeof value[lifecycleMethod] !== "function") {
|
|
110
104
|
throw new Error(`${label}.${lifecycleMethod} must be a function or null.`);
|
|
111
105
|
}
|
|
@@ -169,6 +163,7 @@ async function validateProviderList(packageRecord, side) {
|
|
|
169
163
|
async function validateMigrations(packageRecord, migrationOwners) {
|
|
170
164
|
const { packageJson, packageRoot } = packageRecord;
|
|
171
165
|
const directories = packageJson.jskit?.migrations?.directories;
|
|
166
|
+
validateMigrationsAfter(packageJson);
|
|
172
167
|
if (typeof directories === "undefined") {
|
|
173
168
|
return;
|
|
174
169
|
}
|
|
@@ -180,39 +175,75 @@ async function validateMigrations(packageRecord, migrationOwners) {
|
|
|
180
175
|
throw new Error(`${packageJson.name} migration directory must stay inside its package.`);
|
|
181
176
|
}
|
|
182
177
|
const absoluteDirectory = path.resolve(packageRoot, relativeDirectory);
|
|
183
|
-
const entries = await
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
const entries = await validateMigrationDirectory(packageJson.name, absoluteDirectory, relativeDirectory, migrationOwners);
|
|
179
|
+
// A constraints/ subdirectory runs after the application's migrations; see knexMigrationConfig.js.
|
|
180
|
+
if (entries.some((entry) => entry.isDirectory() && entry.name === "constraints")) {
|
|
181
|
+
await validateMigrationDirectory(
|
|
182
|
+
packageJson.name,
|
|
183
|
+
path.join(absoluteDirectory, "constraints"),
|
|
184
|
+
path.posix.join(relativeDirectory, "constraints"),
|
|
185
|
+
migrationOwners
|
|
186
|
+
);
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// jskit.migrations.after names packages whose migrations the package's own follow; see
|
|
192
|
+
// knexMigrationConfig.js. A catalog package depends on each of them.
|
|
193
|
+
function validateMigrationsAfter(packageJson) {
|
|
194
|
+
const after = packageJson.jskit?.migrations?.after;
|
|
195
|
+
if (typeof after === "undefined") {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (typeof packageJson.jskit.migrations.directories === "undefined") {
|
|
199
|
+
throw new Error(`${packageJson.name}#jskit.migrations.after needs jskit.migrations.directories.`);
|
|
200
|
+
}
|
|
201
|
+
for (const prerequisite of requireStringArray(after, `${packageJson.name}#jskit.migrations.after`)) {
|
|
202
|
+
if (prerequisite === packageJson.name) {
|
|
203
|
+
throw new Error(`${packageJson.name}#jskit.migrations.after names the package itself.`);
|
|
190
204
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
if (!Object.hasOwn(packageJson.dependencies || {}, prerequisite) &&
|
|
206
|
+
!Object.hasOwn(packageJson.peerDependencies || {}, prerequisite)) {
|
|
207
|
+
throw new Error(`${packageJson.name}#jskit.migrations.after names ${prerequisite}, which it does not depend on.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
197
211
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
async function validateMigrationDirectory(packageName, absoluteDirectory, relativeDirectory, migrationOwners) {
|
|
213
|
+
const entries = await readdir(absoluteDirectory, { withFileTypes: true }).catch(() => null);
|
|
214
|
+
if (!entries) {
|
|
215
|
+
throw new Error(`${packageName} migration directory is missing: ${relativeDirectory}.`);
|
|
216
|
+
}
|
|
217
|
+
const migrations = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".cjs"));
|
|
218
|
+
if (migrations.length < 1) {
|
|
219
|
+
throw new Error(`${packageName} migration directory contains no .cjs migrations.`);
|
|
220
|
+
}
|
|
221
|
+
for (const migration of migrations.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
222
|
+
const previous = migrationOwners.get(migration.name);
|
|
223
|
+
if (previous) {
|
|
224
|
+
throw new Error(`Migration filename ${migration.name} is owned by both ${previous} and ${packageName}.`);
|
|
225
|
+
}
|
|
226
|
+
migrationOwners.set(migration.name, packageName);
|
|
227
|
+
|
|
228
|
+
const migrationPath = path.join(absoluteDirectory, migration.name);
|
|
229
|
+
let namespace;
|
|
230
|
+
try {
|
|
231
|
+
namespace = await import(pathToFileURL(migrationPath).href);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`${packageName} migration ${migration.name} could not load: ${error instanceof Error ? error.message : String(error)}`,
|
|
235
|
+
{ cause: error }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const migrationModule = namespace.default && typeof namespace.default === "object"
|
|
239
|
+
? namespace.default
|
|
240
|
+
: namespace;
|
|
241
|
+
// readable-check: allow method-probe - input validation: a package's migration module is checked before the catalog lists it
|
|
242
|
+
if (typeof migrationModule.up !== "function" || typeof migrationModule.down !== "function") {
|
|
243
|
+
throw new Error(`${packageName} migration ${migration.name} must export up() and down().`);
|
|
214
244
|
}
|
|
215
245
|
}
|
|
246
|
+
return entries;
|
|
216
247
|
}
|
|
217
248
|
|
|
218
249
|
async function validatePackage(packageRecord, localVersions, migrationOwners) {
|