@jskit-ai/jskit-catalog 0.2.44 → 0.2.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-catalog",
3
- "version": "0.2.44",
3
+ "version": "0.2.45",
4
4
  "description": "Published metadata catalog for JSKIT package metadata.",
5
5
  "type": "module",
6
6
  "files": [
package/scripts/jskit.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  formatExportSummary
13
13
  } from "./i18n.mjs";
14
14
  import { checkDataAccess, formatDataAccessReport } from "./data-access.mjs";
15
+ import { checkManifestCapabilities, formatManifestCapabilitiesReport } from "./manifest-capabilities.mjs";
15
16
  import { checkMaterial3, formatMaterial3Report } from "./material3.mjs";
16
17
  import { checkProject, updateProject } from "./project-packages.mjs";
17
18
  import { runTests } from "./test-command.mjs";
@@ -50,7 +51,10 @@ function usage() {
50
51
  " installed JSKIT packages share a table or a name (data/one-resource-per-table), or when a system",
51
52
  " scope (a feature's systemScope, a systemContext() call) gives no literal reason (data/system-scope),",
52
53
  " and it lists every system scope and every file the infrastructure exception lifts past the",
53
- " data-access lint rules for review.",
54
+ " data-access lint rules for review. It fails when an application package's manifest",
55
+ " (jskit.capabilities) misses a capability its features (server providers) provide or require, or",
56
+ " provides one none of them does (manifest/capabilities), and lists each requirement no feature has,",
57
+ " which only install time may need, for review.",
54
58
  " check m3 Check application UI source against the JSKIT Material 3 guide alone, with each finding's guide",
55
59
  " section and every exception: the whole app, or only the given files and directories (a section,",
56
60
  " or a task's changed files). Plain jskit check applies the same rules to the whole app.",
@@ -272,6 +276,7 @@ async function main(argv = process.argv.slice(2)) {
272
276
  const ui = await checkMaterial3({ projectRoot: options.projectRoot });
273
277
  const tests = await checkTestRuns({ projectRoot: options.projectRoot, manifests: result.manifests });
274
278
  const data = await checkDataAccess({ projectRoot: options.projectRoot });
279
+ const capabilities = await checkManifestCapabilities({ projectRoot: options.projectRoot, manifests: result.manifests });
275
280
  // The data rules first: a graph that fails on many pin versions would otherwise bury them.
276
281
  const reports = [
277
282
  formatDataAccessReport(data),
@@ -280,9 +285,10 @@ async function main(argv = process.argv.slice(2)) {
280
285
  `${result.installations.length} resolved package installation(s).`
281
286
  : `JSKIT project graph is inconsistent:\n- ${result.issues.join("\n- ")}`,
282
287
  formatMaterial3Report(ui),
283
- formatTestRunsReport(tests)
288
+ formatTestRunsReport(tests),
289
+ formatManifestCapabilitiesReport(capabilities)
284
290
  ];
285
- if (!result.ok || !ui.ok || !tests.ok || !data.ok) {
291
+ if (!result.ok || !ui.ok || !tests.ok || !data.ok || !capabilities.ok) {
286
292
  throw new Error(reports.join("\n"));
287
293
  }
288
294
  process.stdout.write(`${reports.join("\n")}\n`);
@@ -0,0 +1,89 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { toPosixPath } from "./files.mjs";
5
+
6
+ // The manifest part of plain `jskit check`: an application package's manifest (`jskit.capabilities` in its
7
+ // package.json) names what its features (the server providers `jskit.runtime.server.providers` declares) provide and
8
+ // require. The server refuses to start on a drifted one; here it fails the check instead, with the capability to add or
9
+ // remove. The kernel the package itself resolves loads the features and compares them, by the rules it applies at
10
+ // start-up, so the two never disagree.
11
+
12
+ const GUIDE = "https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features#package-manifest";
13
+ const KERNEL_PLATFORM = "@jskit-ai/kernel/server/platform";
14
+
15
+ // The line naming a capability in a manifest's `capabilities` block, else the block's own line.
16
+ function capabilityLine(contents, capabilityId = "") {
17
+ const text = String(contents);
18
+ const block = text.indexOf('"capabilities"');
19
+ if (block < 0) return 1;
20
+ const named = capabilityId ? text.indexOf(`"${capabilityId}"`, block) : -1;
21
+ return text.slice(0, named < 0 ? block : named).split("\n").length;
22
+ }
23
+
24
+ // The kernel's platform module as the package resolves it, else as the app does; null when neither has one.
25
+ async function kernelPlatform(manifestPath, root) {
26
+ for (const base of [manifestPath, path.join(root, "package.json")]) {
27
+ let resolved;
28
+ try {
29
+ resolved = createRequire(base).resolve(KERNEL_PLATFORM);
30
+ } catch {
31
+ continue;
32
+ }
33
+ return await import(pathToFileURL(resolved).href);
34
+ }
35
+ return null;
36
+ }
37
+
38
+ async function checkManifestCapabilities({ projectRoot, manifests }) {
39
+ const root = path.resolve(projectRoot);
40
+ const findings = [];
41
+ const installTime = [];
42
+ let packages = 0;
43
+ for (const manifest of manifests) {
44
+ const jskit = manifest.value?.jskit;
45
+ if (!Array.isArray(jskit?.runtime?.server?.providers) || jskit.runtime.server.providers.length < 1) continue;
46
+ const file = toPosixPath(path.relative(root, manifest.absolutePath)) || "package.json";
47
+ const at = (capabilityId, message) => findings.push({ file, line: capabilityLine(manifest.contents, capabilityId), rule: "manifest/capabilities", message });
48
+ const kernel = await kernelPlatform(manifest.absolutePath, root);
49
+ if (typeof kernel?.packageCapabilityDrift !== "function") {
50
+ at("", `declares server providers, but no installed ${KERNEL_PLATFORM} compares a manifest with them; run jskit update.`);
51
+ continue;
52
+ }
53
+ let drift;
54
+ try {
55
+ drift = await kernel.packageCapabilityDrift({
56
+ packageId: String(manifest.value.name || file),
57
+ packageRoot: path.dirname(manifest.absolutePath),
58
+ packageMetadata: jskit
59
+ });
60
+ } catch (error) {
61
+ at("", `declares server providers that do not load: ${error instanceof Error ? error.message : String(error)}`);
62
+ continue;
63
+ }
64
+ if (!drift) continue;
65
+ packages += 1;
66
+ for (const id of drift.unlistedProvides) at("", `its features provide ${id}, which jskit.capabilities.provides does not list; add it.`);
67
+ for (const id of drift.extraProvides) at(id, `jskit.capabilities.provides lists ${id}, which none of its features provides; remove it.`);
68
+ for (const id of drift.unlistedRequires) at("", `its features require ${id}, which jskit.capabilities.requires does not list; add it.`);
69
+ for (const id of drift.extraRequires) installTime.push({ file, line: capabilityLine(manifest.contents, id), capabilityId: id });
70
+ }
71
+ findings.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line);
72
+ return { ok: findings.length < 1, findings, installTime, packages };
73
+ }
74
+
75
+ function formatManifestCapabilitiesReport(result) {
76
+ const lines = result.findings.flatMap(({ file, line, rule, message }) => [`${file}:${line} ${rule} ${message}`, ` ${GUIDE}`]);
77
+ if (result.installTime.length > 0) {
78
+ lines.push(`Manifest requirements no feature has (${result.installTime.length}), each needed only at install time, such as ` +
79
+ `the database a migration needs; remove any other (${GUIDE}):`);
80
+ for (const { file, line, capabilityId } of result.installTime) lines.push(` ${file}:${line} ${capabilityId}`);
81
+ }
82
+ lines.push(result.ok
83
+ ? `JSKIT package manifests name what their features provide and require in ${result.packages} package(s).`
84
+ : `JSKIT package manifests: ${result.findings.length} error(s). A package's jskit.capabilities lists exactly what its ` +
85
+ "features provide, and every capability they require beyond the kernel's own and the package's.");
86
+ return lines.join("\n");
87
+ }
88
+
89
+ export { checkManifestCapabilities, formatManifestCapabilitiesReport };