@jskit-ai/jskit-catalog 0.2.35 → 0.2.36
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 +477 -384
- package/package.json +1 -1
- package/scripts/browser-tests.mjs +16 -3
- package/scripts/data-access.mjs +564 -0
- package/scripts/jskit.mjs +13 -2
- package/scripts/project-packages.mjs +65 -3
- 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 {
|
|
@@ -253,7 +261,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
253
261
|
});
|
|
254
262
|
const ui = await checkMaterial3({ projectRoot: options.projectRoot, navigationOnly: !MATERIAL3_GATE });
|
|
255
263
|
const tests = await checkTestRuns({ projectRoot: options.projectRoot, manifests: result.manifests });
|
|
264
|
+
const data = await checkDataAccess({ projectRoot: options.projectRoot });
|
|
265
|
+
// The data rules first: a graph that fails on many pin versions would otherwise bury them.
|
|
256
266
|
const reports = [
|
|
267
|
+
formatDataAccessReport(data),
|
|
257
268
|
result.ok
|
|
258
269
|
? `JSKIT project graph is coordinated across ${result.manifests.length} manifest(s) and ` +
|
|
259
270
|
`${result.installations.length} resolved package installation(s).`
|
|
@@ -261,7 +272,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
261
272
|
MATERIAL3_GATE ? formatMaterial3Report(ui) : formatNavigationReport(ui),
|
|
262
273
|
formatTestRunsReport(tests)
|
|
263
274
|
];
|
|
264
|
-
if (!result.ok || !ui.ok || !tests.ok) {
|
|
275
|
+
if (!result.ok || !ui.ok || !tests.ok || !data.ok) {
|
|
265
276
|
throw new Error(reports.join("\n"));
|
|
266
277
|
}
|
|
267
278
|
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,39 @@ 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
|
+
|
|
398
441
|
/**
|
|
399
442
|
* Pins a project's JSKIT dependencies to the catalog and installs them; a
|
|
400
443
|
* failed install or check restores the manifests, lockfile and node_modules.
|
|
@@ -414,10 +457,20 @@ async function updateProject({
|
|
|
414
457
|
const localPackageNames = collectLocalPackageNames(manifests);
|
|
415
458
|
const declarations = collectManifestDeclarations(manifests, root)
|
|
416
459
|
.filter((entry) => !localPackageNames.has(entry.packageId));
|
|
460
|
+
const retiredPackageIds = [...new Set(
|
|
461
|
+
declarations.map((entry) => entry.packageId).filter((packageId) => Object.hasOwn(RETIRED_PACKAGES, packageId))
|
|
462
|
+
)].sort();
|
|
463
|
+
const retiredImports = retiredPackageIds.length > 0 ? await findImports(root, retiredPackageIds) : new Map();
|
|
464
|
+
if (retiredImports.size > 0) {
|
|
465
|
+
throw new Error(`Cannot update while the application imports retired JSKIT packages:\n${[...retiredImports]
|
|
466
|
+
.map(([packageId, files]) => `- ${packageId} (in ${files.sort().join(", ")}): ${RETIRED_PACKAGES[packageId].packageId} ` +
|
|
467
|
+
`replaced it; import from ${RETIRED_PACKAGES[packageId].importFrom} instead.`)
|
|
468
|
+
.join("\n")}`);
|
|
469
|
+
}
|
|
417
470
|
const unknownPackageIds = [...new Set(
|
|
418
471
|
declarations
|
|
419
472
|
.map((entry) => entry.packageId)
|
|
420
|
-
.filter((packageId) => !normalizedCatalog.versions.has(packageId))
|
|
473
|
+
.filter((packageId) => !normalizedCatalog.versions.has(packageId) && !retiredPackageIds.includes(packageId))
|
|
421
474
|
)].sort();
|
|
422
475
|
if (unknownPackageIds.length > 0) {
|
|
423
476
|
throw new Error(`Cannot update JSKIT packages missing from the coordinated catalog: ${unknownPackageIds.join(", ")}.`);
|
|
@@ -449,6 +502,11 @@ async function updateProject({
|
|
|
449
502
|
if (!packageId.startsWith(JSKIT_SCOPE_PREFIX) || localPackageNames.has(packageId)) {
|
|
450
503
|
continue;
|
|
451
504
|
}
|
|
505
|
+
if (retiredPackageIds.includes(packageId)) {
|
|
506
|
+
delete next[field][packageId];
|
|
507
|
+
changed = true;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
452
510
|
const expectedVersion = normalizedCatalog.versions.get(packageId);
|
|
453
511
|
if (next[field][packageId] !== expectedVersion) {
|
|
454
512
|
next[field][packageId] = expectedVersion;
|
|
@@ -527,7 +585,11 @@ async function updateProject({
|
|
|
527
585
|
changedFiles: Object.freeze(
|
|
528
586
|
updates.map((update) => toPosixPath(path.relative(root, update.manifest.absolutePath)))
|
|
529
587
|
),
|
|
530
|
-
catalogVersion
|
|
588
|
+
catalogVersion,
|
|
589
|
+
retiredPackages: Object.freeze(retiredPackageIds.map((packageId) => Object.freeze({
|
|
590
|
+
packageId,
|
|
591
|
+
replacedBy: RETIRED_PACKAGES[packageId].packageId
|
|
592
|
+
})))
|
|
531
593
|
});
|
|
532
594
|
}
|
|
533
595
|
|
|
@@ -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,
|