@jskit-ai/jskit-catalog 0.2.34 → 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.
@@ -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,