@jskit-ai/jskit-catalog 0.2.35 → 0.2.37
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 +480 -386
- package/package.json +1 -1
- package/scripts/browser-tests.mjs +16 -3
- package/scripts/data-access.mjs +738 -0
- package/scripts/jskit.mjs +24 -3
- package/scripts/project-packages.mjs +96 -3
- package/scripts/test-databases.mjs +76 -2
- package/scripts/verify-packages.mjs +23 -0
package/scripts/jskit.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
formatCheckReport,
|
|
12
12
|
formatExportSummary
|
|
13
13
|
} from "./i18n.mjs";
|
|
14
|
+
import { checkDataAccess, formatDataAccessReport } from "./data-access.mjs";
|
|
14
15
|
import { checkMaterial3, formatMaterial3Report, formatNavigationReport } from "./material3.mjs";
|
|
15
16
|
import { checkProject, updateProject } from "./project-packages.mjs";
|
|
16
17
|
import { runTests } from "./test-command.mjs";
|
|
@@ -45,7 +46,11 @@ function usage() {
|
|
|
45
46
|
" workers do not come from testConcurrency() of @jskit-ai/jskit-catalog/test-budget. A Playwright",
|
|
46
47
|
" config with a webServer uses browserTestConfig() of @jskit-ai/jskit-catalog/browser-tests (one",
|
|
47
48
|
" production-build server and database per worker), never a development server, and every one",
|
|
48
|
-
" has a globalTimeout.",
|
|
49
|
+
" has a globalTimeout. It fails when two table resources (defineTableResource) of the app and its",
|
|
50
|
+
" installed JSKIT packages share a table or a name (data/one-resource-per-table), or when a system",
|
|
51
|
+
" scope (a feature's systemScope, a systemContext() call) gives no literal reason (data/system-scope),",
|
|
52
|
+
" and it lists every system scope and every file the infrastructure exception lifts past the",
|
|
53
|
+
" data-access lint rules for review.",
|
|
49
54
|
" check m3 List application UI source that departs from the JSKIT Material 3 guide, each finding with its",
|
|
50
55
|
" guide section: the whole app, or only the given files and directories (a section, or a task's",
|
|
51
56
|
" changed files). Its visual rules (m3/*) are a report; its nav/* rules are plain jskit check's.",
|
|
@@ -231,6 +236,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
231
236
|
additionalManifests: options.additionalManifests,
|
|
232
237
|
install: options.install
|
|
233
238
|
});
|
|
239
|
+
for (const { packageId, replacedBy } of result.retiredPackages) {
|
|
240
|
+
process.stdout.write(`Removed ${packageId}, which this release retired: ${replacedBy} replaced it.\n`);
|
|
241
|
+
}
|
|
234
242
|
if (result.changedFiles.length < 1) {
|
|
235
243
|
process.stdout.write(`JSKIT manifests already match catalog ${result.catalogVersion}.\n`);
|
|
236
244
|
} else {
|
|
@@ -239,8 +247,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
239
247
|
`${result.changedFiles.map((file) => `- ${file}`).join("\n")}\n`
|
|
240
248
|
);
|
|
241
249
|
}
|
|
250
|
+
if (result.addedMigrations.length > 0) {
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
`The update installed ${result.addedMigrations.length} new package migration(s); run npm run db:migrate before ` +
|
|
253
|
+
`the app serves requests, since the new packages read what they create:\n` +
|
|
254
|
+
`${result.addedMigrations.map((entry) => `- ${entry}`).join("\n")}\n`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
242
257
|
if (!options.install) {
|
|
243
|
-
process.stdout.write(
|
|
258
|
+
process.stdout.write(
|
|
259
|
+
"Skipped npm install; run it before jskit check. If it installs new package migrations, run npm run db:migrate " +
|
|
260
|
+
"before the app serves requests.\n"
|
|
261
|
+
);
|
|
244
262
|
}
|
|
245
263
|
return;
|
|
246
264
|
}
|
|
@@ -253,7 +271,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
253
271
|
});
|
|
254
272
|
const ui = await checkMaterial3({ projectRoot: options.projectRoot, navigationOnly: !MATERIAL3_GATE });
|
|
255
273
|
const tests = await checkTestRuns({ projectRoot: options.projectRoot, manifests: result.manifests });
|
|
274
|
+
const data = await checkDataAccess({ projectRoot: options.projectRoot });
|
|
275
|
+
// The data rules first: a graph that fails on many pin versions would otherwise bury them.
|
|
256
276
|
const reports = [
|
|
277
|
+
formatDataAccessReport(data),
|
|
257
278
|
result.ok
|
|
258
279
|
? `JSKIT project graph is coordinated across ${result.manifests.length} manifest(s) and ` +
|
|
259
280
|
`${result.installations.length} resolved package installation(s).`
|
|
@@ -261,7 +282,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
261
282
|
MATERIAL3_GATE ? formatMaterial3Report(ui) : formatNavigationReport(ui),
|
|
262
283
|
formatTestRunsReport(tests)
|
|
263
284
|
];
|
|
264
|
-
if (!result.ok || !ui.ok || !tests.ok) {
|
|
285
|
+
if (!result.ok || !ui.ok || !tests.ok || !data.ok) {
|
|
265
286
|
throw new Error(reports.join("\n"));
|
|
266
287
|
}
|
|
267
288
|
process.stdout.write(`${reports.join("\n")}\n`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileExists, toPosixPath } from "./files.mjs";
|
|
4
4
|
import { readOptionalFile, restoreProjectFiles, runNpmInstall, writeManifestUpdates } from "./project-install.mjs";
|
|
@@ -13,6 +13,16 @@ const DEPENDENCY_FIELDS = Object.freeze([
|
|
|
13
13
|
"optionalDependencies"
|
|
14
14
|
]);
|
|
15
15
|
const UPDATE_SCRIPT = "npx --yes @jskit-ai/jskit-catalog@latest update";
|
|
16
|
+
// JSKIT packages a release retired, each with what replaced it. The updater removes their declarations itself, and
|
|
17
|
+
// refuses while the application's own code still imports one, naming the import to use instead.
|
|
18
|
+
const RETIRED_PACKAGES = Object.freeze({
|
|
19
|
+
"@jskit-ai/json-rest-api-core": Object.freeze({
|
|
20
|
+
packageId: "@jskit-ai/resource-runtime",
|
|
21
|
+
importFrom: "@jskit-ai/resource-runtime/server (its types from @jskit-ai/resource-runtime/server/types)"
|
|
22
|
+
})
|
|
23
|
+
});
|
|
24
|
+
const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]s|vue)$/u;
|
|
25
|
+
const SKIPPED_DIRECTORIES = new Set(["node_modules", "dist", "build", "coverage"]);
|
|
16
26
|
const CHECK_SCRIPT = "jskit check";
|
|
17
27
|
|
|
18
28
|
function normalizeCatalog(catalog = {}) {
|
|
@@ -395,6 +405,65 @@ async function preparePackageLockForInstall(projectRoot, manifests, packageLockP
|
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The application's files, outside node_modules and build output, that import one of the packages.
|
|
410
|
+
* @param {string} root
|
|
411
|
+
* @param {string[]} packageIds
|
|
412
|
+
* @returns {Promise<Map<string, string[]>>} The importing files, relative to root, by package.
|
|
413
|
+
*/
|
|
414
|
+
async function findImports(root, packageIds) {
|
|
415
|
+
const imports = new Map();
|
|
416
|
+
const patterns = packageIds.map((packageId) => {
|
|
417
|
+
const name = packageId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
418
|
+
return [packageId, new RegExp(`["']${name}(?:/[^"']*)?["']`, "u")];
|
|
419
|
+
});
|
|
420
|
+
const visit = async (directory) => {
|
|
421
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
422
|
+
const absolutePath = path.join(directory, entry.name);
|
|
423
|
+
if (entry.isDirectory()) {
|
|
424
|
+
if (!entry.name.startsWith(".") && !SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
425
|
+
await visit(absolutePath);
|
|
426
|
+
}
|
|
427
|
+
} else if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) {
|
|
428
|
+
const source = await readFile(absolutePath, "utf8");
|
|
429
|
+
for (const [packageId, pattern] of patterns) {
|
|
430
|
+
if (pattern.test(source)) {
|
|
431
|
+
imports.set(packageId, [...(imports.get(packageId) || []), toPosixPath(path.relative(root, absolutePath))]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
await visit(root);
|
|
438
|
+
return imports;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The migration files the installed @jskit-ai packages ship (jskit.migrations.directories and their constraints/),
|
|
442
|
+
// each "<package>: <path in the package>", as the app's migrations read them from node_modules.
|
|
443
|
+
async function installedPackageMigrations(root) {
|
|
444
|
+
const scope = path.join(root, "node_modules", JSKIT_SCOPE_PREFIX);
|
|
445
|
+
const names = await readdir(scope).catch((error) => {
|
|
446
|
+
if (error?.code === "ENOENT") return [];
|
|
447
|
+
throw error;
|
|
448
|
+
});
|
|
449
|
+
const migrations = new Set();
|
|
450
|
+
for (const name of names) {
|
|
451
|
+
const packageRoot = path.join(scope, name);
|
|
452
|
+
const manifestPath = path.join(packageRoot, "package.json");
|
|
453
|
+
if (!(await fileExists(manifestPath))) continue;
|
|
454
|
+
const directories = (await readJsonRecord(manifestPath)).value.jskit?.migrations?.directories;
|
|
455
|
+
for (const directory of Array.isArray(directories) ? directories : []) {
|
|
456
|
+
for (const folder of [directory, path.join(directory, "constraints")]) {
|
|
457
|
+
const files = await readdir(path.join(packageRoot, folder)).catch(() => []);
|
|
458
|
+
for (const file of files.filter((entry) => entry.endsWith(".cjs"))) {
|
|
459
|
+
migrations.add(`${JSKIT_SCOPE_PREFIX}${name}: ${toPosixPath(path.join(folder, file))}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return migrations;
|
|
465
|
+
}
|
|
466
|
+
|
|
398
467
|
/**
|
|
399
468
|
* Pins a project's JSKIT dependencies to the catalog and installs them; a
|
|
400
469
|
* failed install or check restores the manifests, lockfile and node_modules.
|
|
@@ -414,10 +483,20 @@ async function updateProject({
|
|
|
414
483
|
const localPackageNames = collectLocalPackageNames(manifests);
|
|
415
484
|
const declarations = collectManifestDeclarations(manifests, root)
|
|
416
485
|
.filter((entry) => !localPackageNames.has(entry.packageId));
|
|
486
|
+
const retiredPackageIds = [...new Set(
|
|
487
|
+
declarations.map((entry) => entry.packageId).filter((packageId) => Object.hasOwn(RETIRED_PACKAGES, packageId))
|
|
488
|
+
)].sort();
|
|
489
|
+
const retiredImports = retiredPackageIds.length > 0 ? await findImports(root, retiredPackageIds) : new Map();
|
|
490
|
+
if (retiredImports.size > 0) {
|
|
491
|
+
throw new Error(`Cannot update while the application imports retired JSKIT packages:\n${[...retiredImports]
|
|
492
|
+
.map(([packageId, files]) => `- ${packageId} (in ${files.sort().join(", ")}): ${RETIRED_PACKAGES[packageId].packageId} ` +
|
|
493
|
+
`replaced it; import from ${RETIRED_PACKAGES[packageId].importFrom} instead.`)
|
|
494
|
+
.join("\n")}`);
|
|
495
|
+
}
|
|
417
496
|
const unknownPackageIds = [...new Set(
|
|
418
497
|
declarations
|
|
419
498
|
.map((entry) => entry.packageId)
|
|
420
|
-
.filter((packageId) => !normalizedCatalog.versions.has(packageId))
|
|
499
|
+
.filter((packageId) => !normalizedCatalog.versions.has(packageId) && !retiredPackageIds.includes(packageId))
|
|
421
500
|
)].sort();
|
|
422
501
|
if (unknownPackageIds.length > 0) {
|
|
423
502
|
throw new Error(`Cannot update JSKIT packages missing from the coordinated catalog: ${unknownPackageIds.join(", ")}.`);
|
|
@@ -449,6 +528,11 @@ async function updateProject({
|
|
|
449
528
|
if (!packageId.startsWith(JSKIT_SCOPE_PREFIX) || localPackageNames.has(packageId)) {
|
|
450
529
|
continue;
|
|
451
530
|
}
|
|
531
|
+
if (retiredPackageIds.includes(packageId)) {
|
|
532
|
+
delete next[field][packageId];
|
|
533
|
+
changed = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
452
536
|
const expectedVersion = normalizedCatalog.versions.get(packageId);
|
|
453
537
|
if (next[field][packageId] !== expectedVersion) {
|
|
454
538
|
next[field][packageId] = expectedVersion;
|
|
@@ -486,8 +570,11 @@ async function updateProject({
|
|
|
486
570
|
|
|
487
571
|
const packageLockPath = path.join(root, "package-lock.json");
|
|
488
572
|
const packageLockSnapshot = install ? await readOptionalFile(packageLockPath) : null;
|
|
573
|
+
// npm install replaces node_modules, so the migrations the packages shipped are read before it.
|
|
574
|
+
const migrationsBefore = install ? await installedPackageMigrations(root) : new Set();
|
|
489
575
|
await writeManifestUpdates(updates);
|
|
490
576
|
let installTransaction = null;
|
|
577
|
+
let addedMigrations = [];
|
|
491
578
|
try {
|
|
492
579
|
if (install) {
|
|
493
580
|
const updatedManifests = await discoverProjectManifests(root, additionalManifests);
|
|
@@ -497,6 +584,7 @@ async function updateProject({
|
|
|
497
584
|
if (!result.ok) {
|
|
498
585
|
throw new Error(`JSKIT graph remains inconsistent after npm install:\n- ${result.issues.join("\n- ")}`);
|
|
499
586
|
}
|
|
587
|
+
addedMigrations = [...await installedPackageMigrations(root)].filter((entry) => !migrationsBefore.has(entry)).sort();
|
|
500
588
|
await installTransaction.commit();
|
|
501
589
|
}
|
|
502
590
|
} catch (error) {
|
|
@@ -527,7 +615,12 @@ async function updateProject({
|
|
|
527
615
|
changedFiles: Object.freeze(
|
|
528
616
|
updates.map((update) => toPosixPath(path.relative(root, update.manifest.absolutePath)))
|
|
529
617
|
),
|
|
530
|
-
catalogVersion
|
|
618
|
+
catalogVersion,
|
|
619
|
+
addedMigrations: Object.freeze(addedMigrations),
|
|
620
|
+
retiredPackages: Object.freeze(retiredPackageIds.map((packageId) => Object.freeze({
|
|
621
|
+
packageId,
|
|
622
|
+
replacedBy: RETIRED_PACKAGES[packageId].packageId
|
|
623
|
+
})))
|
|
531
624
|
});
|
|
532
625
|
}
|
|
533
626
|
|
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { readTableResourceSources } from "./data-access.mjs";
|
|
6
7
|
|
|
7
8
|
// `jskit test --databases`: each test file running at once gets a database of its own, a copy of TEST_DB_NAME restored
|
|
8
9
|
// from a schema snapshot, from the application's installed @jskit-ai/database-runtime and its knexfile.js, which loads
|
|
@@ -13,6 +14,9 @@ import { pathToFileURL } from "node:url";
|
|
|
13
14
|
const OPTION = "--databases";
|
|
14
15
|
const KNEXFILES = Object.freeze(["knexfile.js", "knexfile.mjs", "knexfile.cjs"]);
|
|
15
16
|
const TEST_DATABASES = "@jskit-ai/database-runtime/server/testDatabases";
|
|
17
|
+
const RESOURCE_RUNTIME = "@jskit-ai/resource-runtime/server";
|
|
18
|
+
const TABLE_RESOURCES = "@jskit-ai/resource-core/shared/tableResource";
|
|
19
|
+
const GUIDE = "https://mobily-enterprises.github.io/jskit-ai/guide/framework/data-access#layer-1-the-table-resource";
|
|
16
20
|
|
|
17
21
|
/** Runs `load` and puts process.env back as it was, returning what `load` returned and the environment it saw. */
|
|
18
22
|
async function withOwnEnvironment(load) {
|
|
@@ -26,6 +30,76 @@ async function withOwnEnvironment(load) {
|
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
// A module's exported table resources: each export, or each item of an exported list or object.
|
|
34
|
+
function exportedTableResources(namespace, isTableResource) {
|
|
35
|
+
return Object.values(namespace).flatMap((value) => {
|
|
36
|
+
if (isTableResource(value)) return [value];
|
|
37
|
+
const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
|
|
38
|
+
return items.filter((item) => isTableResource(item));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const keyText = ({ tableName, columns, referencedTableName, referencedColumns }) =>
|
|
43
|
+
`${tableName} (${columns.join(", ")}) -> ${referencedTableName} (${referencedColumns.join(", ")})`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compares the foreignKeys the app's table resources and its @jskit-ai packages' declare with the keys TEST_DB_NAME
|
|
47
|
+
* holds, once a run's databases are ready: json-rest-api relies on a declaration to skip work, so a declared key the
|
|
48
|
+
* database lacks fails the run. A package's table that the database does not have is left out: node_modules can hold
|
|
49
|
+
* a package whose migrations the app never runs (one only a devDependency brings, or one left behind), and the app
|
|
50
|
+
* never reads that table. The app's own tables are always checked. A key of one of the app's own tables that nothing
|
|
51
|
+
* declares is reported, as work json-rest-api could skip. It imports only the modules that declare foreignKeys; an app
|
|
52
|
+
* without the resource runtime has nothing to check.
|
|
53
|
+
* @param {string} cwd
|
|
54
|
+
* @returns {(knex: object) => Promise<string>}
|
|
55
|
+
*/
|
|
56
|
+
function foreignKeyCheckFor(cwd) {
|
|
57
|
+
return async (knex) => {
|
|
58
|
+
const appRequire = createRequire(path.join(cwd, "package.json"));
|
|
59
|
+
let runtime;
|
|
60
|
+
let tableResources;
|
|
61
|
+
try {
|
|
62
|
+
[runtime, tableResources] = [appRequire.resolve(RESOURCE_RUNTIME), appRequire.resolve(TABLE_RESOURCES)];
|
|
63
|
+
} catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const sources = await readTableResourceSources({ projectRoot: cwd });
|
|
67
|
+
const tables = sources.filter((source) => !source.framework && source.tableName).map((source) => source.tableName);
|
|
68
|
+
const declaringFiles = [...new Set(sources.filter((source) => source.foreignKeys).map((source) => source.absolute))];
|
|
69
|
+
if (tables.length < 1 && declaringFiles.length < 1) return "";
|
|
70
|
+
const { isTableResource } = await import(pathToFileURL(tableResources).href);
|
|
71
|
+
const resources = [];
|
|
72
|
+
const absentPackageTables = new Set();
|
|
73
|
+
for (const file of declaringFiles) {
|
|
74
|
+
const declaring = sources.filter((entry) => entry.absolute === file && entry.foreignKeys);
|
|
75
|
+
const exported = exportedTableResources(await import(pathToFileURL(file).href), isTableResource);
|
|
76
|
+
for (const source of declaring) {
|
|
77
|
+
if (!exported.some((resource) => resource.foreignKeys && resource.name === source.name)) {
|
|
78
|
+
throw new Error(`${source.file}:${source.line} declares foreignKeys on a table resource the module does not export, so ` +
|
|
79
|
+
`jskit test ${OPTION} cannot check them against the database: export the resource.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const resource of exported.filter((entry) => entry.foreignKeys)) {
|
|
83
|
+
if (declaring[0].framework && !(await knex.schema.hasTable(resource.tableName))) absentPackageTables.add(resource.tableName);
|
|
84
|
+
else resources.push(resource);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const { checkForeignKeys } = await import(pathToFileURL(runtime).href);
|
|
88
|
+
const { missing, undeclared } = await checkForeignKeys({ knex, resources: [...new Set(resources)], tables });
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
throw new Error(`jskit test ${OPTION}: ${missing.length} declared foreign key(s) the test database does not hold. ` +
|
|
91
|
+
`json-rest-api relies on a declared key, so a wrong one changes results: fix the declaration or add the key ` +
|
|
92
|
+
`with a migration (${GUIDE}).\n` + missing.map((key) => ` ${key.resource}: ${keyText(key)}`).join("\n"));
|
|
93
|
+
}
|
|
94
|
+
const declared = [...new Set(resources)].reduce((count, resource) => count + resource.foreignKeys.length, 0);
|
|
95
|
+
const checked = `foreign keys: ${declared} declared, all in the database` + (absentPackageTables.size < 1 ? "" :
|
|
96
|
+
` (${absentPackageTables.size} JSKIT package table(s) the database does not have are not checked)`);
|
|
97
|
+
if (undeclared.length < 1) return checked;
|
|
98
|
+
return `${checked}; ${undeclared.length} database key(s) on the app's tables are not declared, so json-rest-api cannot ` +
|
|
99
|
+
`skip the work they would save (declare each on its table resource's foreignKeys): ${undeclared.map(keyText).join("; ")}`;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
29
103
|
/**
|
|
30
104
|
* The run's databases for `count` files at once, or one file at a time on the connected database.
|
|
31
105
|
* @param {number} count at most the run's test budget, and no more than its test files
|
|
@@ -44,7 +118,7 @@ async function testDatabaseRunFor(count, cwd = process.cwd()) {
|
|
|
44
118
|
const { result: { default: knexConfig, testDatabaseSchema: schema }, environment } = await withOwnEnvironment(async () =>
|
|
45
119
|
import(pathToFileURL(knexfile).href));
|
|
46
120
|
const { testDatabaseRun } = await import(pathToFileURL(testDatabases).href);
|
|
47
|
-
return testDatabaseRun({ knexConfig, environment, count, schema, appRoot: cwd });
|
|
121
|
+
return testDatabaseRun({ knexConfig, environment, count, schema, appRoot: cwd, checkDatabase: foreignKeyCheckFor(cwd) });
|
|
48
122
|
}
|
|
49
123
|
|
|
50
|
-
export { OPTION as DATABASES_OPTION, testDatabaseRunFor };
|
|
124
|
+
export { OPTION as DATABASES_OPTION, foreignKeyCheckFor, testDatabaseRunFor };
|
|
@@ -249,6 +249,17 @@ async function validateMigrationDirectory(packageName, absoluteDirectory, relati
|
|
|
249
249
|
return entries;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// Each apiSummary surface names an entrypoint the package exports, or a group of them ("./server" for its "./server/..."
|
|
253
|
+
// subpaths), so the catalog never describes an import that fails.
|
|
254
|
+
function validateApiSummarySurfaces(packageJson) {
|
|
255
|
+
const exported = packageJson.exports && typeof packageJson.exports === "object" ? Object.keys(packageJson.exports) : [];
|
|
256
|
+
for (const { subpath } of packageJson.jskit?.metadata?.apiSummary?.surfaces || []) {
|
|
257
|
+
if (!exported.some((entry) => entry === subpath || entry.startsWith(`${subpath}/`))) {
|
|
258
|
+
throw new Error(`${packageJson.name}#jskit.metadata.apiSummary describes ${subpath}, which the package does not export.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
252
263
|
async function validatePackage(packageRecord, localVersions, migrationOwners) {
|
|
253
264
|
const { packageJson, packageRoot, manifestPath } = packageRecord;
|
|
254
265
|
const packageId = String(packageJson.name || "").trim();
|
|
@@ -282,6 +293,7 @@ async function validatePackage(packageRecord, localVersions, migrationOwners) {
|
|
|
282
293
|
throw new Error(`${packageId} cannot both provide and require the same capability.`);
|
|
283
294
|
}
|
|
284
295
|
|
|
296
|
+
validateApiSummarySurfaces(packageJson);
|
|
285
297
|
const serverProviderCount = await validateProviderList(packageRecord, "server");
|
|
286
298
|
await validateProviderList(packageRecord, "client");
|
|
287
299
|
await validateMigrations(packageRecord, migrationOwners);
|
|
@@ -337,12 +349,22 @@ function validateCapabilityClosure(packages, { builtinCapabilities = BUILTIN_CAP
|
|
|
337
349
|
}
|
|
338
350
|
}
|
|
339
351
|
|
|
352
|
+
// The application's one table runtime: a package that takes it as a dependency can install a second copy of it beside
|
|
353
|
+
// the application's, with its own tables, transactions and error classes. Every package takes it as a peer.
|
|
354
|
+
const TABLE_RUNTIME_PACKAGE_IDS = Object.freeze(["@jskit-ai/database-runtime", "@jskit-ai/resource-runtime"]);
|
|
355
|
+
|
|
340
356
|
function validateSingletonPeerDependencies(packages) {
|
|
341
357
|
const singletonPackageIds = new Set(collectSingletonPackageIds(packages.map(({ packageJson }) => packageJson)));
|
|
342
358
|
|
|
343
359
|
for (const { packageJson } of packages) {
|
|
344
360
|
for (const section of ["dependencies", "optionalDependencies"]) {
|
|
345
361
|
for (const dependencyId of Object.keys(packageJson[section] || {})) {
|
|
362
|
+
if (TABLE_RUNTIME_PACKAGE_IDS.includes(dependencyId)) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`${packageJson.name}#${section}.${dependencyId} must be declared in peerDependencies because ` +
|
|
365
|
+
`${dependencyId} is the application's one table runtime.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
346
368
|
if (!singletonPackageIds.has(dependencyId)) {
|
|
347
369
|
continue;
|
|
348
370
|
}
|
|
@@ -400,6 +422,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === SCRIPT_PATH) {
|
|
|
400
422
|
|
|
401
423
|
export {
|
|
402
424
|
main,
|
|
425
|
+
validateApiSummarySurfaces,
|
|
403
426
|
validateCapabilityClosure,
|
|
404
427
|
validateMigrations,
|
|
405
428
|
validateProviderExport,
|