@jskit-ai/jskit-catalog 0.2.8 → 0.2.10

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.8",
3
+ "version": "0.2.10",
4
4
  "description": "Published metadata catalog for JSKIT package metadata.",
5
5
  "type": "module",
6
6
  "files": [
@@ -7,7 +7,7 @@ import {
7
7
  assertUniquePatternIds,
8
8
  discoverPackagePatterns
9
9
  } from "./pattern-assets.mjs";
10
- import { discoverProjectManifests } from "./project-packages.mjs";
10
+ import { discoverProjectManifests } from "./project-manifests.mjs";
11
11
 
12
12
  const SCRIPT_PATH = fileURLToPath(import.meta.url);
13
13
  const PACKAGE_ROOT = path.resolve(path.dirname(SCRIPT_PATH), "..");
@@ -0,0 +1,17 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ async function fileExists(absolutePath) {
5
+ try {
6
+ await access(absolutePath);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function toPosixPath(value = "") {
14
+ return String(value || "").split(path.sep).join("/");
15
+ }
16
+
17
+ export { fileExists, toPosixPath };
@@ -1,17 +1,9 @@
1
- import { access, readdir, readFile } from "node:fs/promises";
1
+ import { readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileExists } from "./files.mjs";
3
4
 
4
5
  const BUILTIN_SINGLETON_PACKAGE_IDS = Object.freeze(["@jskit-ai/kernel"]);
5
6
 
6
- async function fileExists(filePath) {
7
- try {
8
- await access(filePath);
9
- return true;
10
- } catch {
11
- return false;
12
- }
13
- }
14
-
15
7
  // The JSKIT framework packages are the direct children of packagesRoot whose
16
8
  // package.json declares `jskit` metadata. The catalog builder and the package
17
9
  // verifier both read this one set.
package/scripts/i18n.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { toPosixPath } from "./files.mjs";
3
4
  import { resolveInstalledPackage } from "./project-packages.mjs";
4
5
 
5
6
  // `jskit i18n export|check <locale>`: an app translates JSKIT by supplying
@@ -225,7 +226,7 @@ function nestCatalog(flat, file) {
225
226
  }
226
227
 
227
228
  function relativeFile(projectRoot, file) {
228
- return path.relative(path.resolve(projectRoot), file).split(path.sep).join("/");
229
+ return toPosixPath(path.relative(path.resolve(projectRoot), file));
229
230
  }
230
231
 
231
232
  // --- export ------------------------------------------------------------------------
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { toPosixPath } from "./files.mjs";
3
4
 
4
5
  const PATTERN_DOCUMENT_NAME = "PATTERN.md";
5
6
  const PATTERN_EXAMPLE_DIRECTORY = "example";
@@ -27,10 +28,6 @@ function normalizeText(value = "") {
27
28
  return String(value || "").trim();
28
29
  }
29
30
 
30
- function normalizePath(value = "") {
31
- return String(value || "").split(path.sep).join("/");
32
- }
33
-
34
31
  function sortedUniqueStrings(values = []) {
35
32
  return [...new Set(values.map(normalizeText).filter(Boolean))]
36
33
  .sort((left, right) => left.localeCompare(right));
@@ -156,7 +153,7 @@ async function collectFiles(rootDirectory) {
156
153
  continue;
157
154
  }
158
155
  if (entry.isFile()) {
159
- files.push(normalizePath(path.relative(rootDirectory, absolutePath)));
156
+ files.push(toPosixPath(path.relative(rootDirectory, absolutePath)));
160
157
  }
161
158
  }
162
159
  }
@@ -186,7 +183,7 @@ async function discoverPackagePatterns({ packageRoot, packageJson } = {}) {
186
183
  for (const entry of patternDirectories) {
187
184
  const patternRoot = path.join(patternsRoot, entry.name);
188
185
  const documentPath = path.join(patternRoot, PATTERN_DOCUMENT_NAME);
189
- const relativeDocumentPath = normalizePath(path.relative(resolvedPackageRoot, documentPath));
186
+ const relativeDocumentPath = toPosixPath(path.relative(resolvedPackageRoot, documentPath));
190
187
  let source;
191
188
  try {
192
189
  source = await readFile(documentPath, "utf8");
@@ -220,7 +217,7 @@ async function discoverPackagePatterns({ packageRoot, packageJson } = {}) {
220
217
  packageId,
221
218
  packageVersion,
222
219
  documentPath: relativeDocumentPath,
223
- examplePath: normalizePath(path.relative(resolvedPackageRoot, exampleRoot)),
220
+ examplePath: toPosixPath(path.relative(resolvedPackageRoot, exampleRoot)),
224
221
  files: Object.freeze(files)
225
222
  }));
226
223
  }
@@ -0,0 +1,156 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileExists } from "./files.mjs";
5
+
6
+ // Writing an update's manifests and installing it, each undoable: manifest
7
+ // writes are atomic per file and restorable, and `npm install` runs against a
8
+ // moved-aside node_modules that a rollback puts back.
9
+
10
+ async function writeManifestUpdates(updates = []) {
11
+ const temporaryPaths = [];
12
+ const committed = [];
13
+ try {
14
+ for (const [index, update] of updates.entries()) {
15
+ const temporaryPath = `${update.manifest.absolutePath}.jskit-${process.pid}-${index}.tmp`;
16
+ await writeFile(temporaryPath, update.contents, "utf8");
17
+ temporaryPaths.push(temporaryPath);
18
+ }
19
+ for (const [index, update] of updates.entries()) {
20
+ await rename(temporaryPaths[index], update.manifest.absolutePath);
21
+ committed.push(update);
22
+ }
23
+ } catch (error) {
24
+ for (const update of committed) {
25
+ await writeFile(update.manifest.absolutePath, update.manifest.contents, "utf8");
26
+ }
27
+ throw error;
28
+ } finally {
29
+ await Promise.all(temporaryPaths.map((temporaryPath) => rm(temporaryPath, { force: true })));
30
+ }
31
+ }
32
+
33
+ async function readOptionalFile(absolutePath) {
34
+ try {
35
+ return Object.freeze({
36
+ contents: await readFile(absolutePath, "utf8"),
37
+ exists: true
38
+ });
39
+ } catch (error) {
40
+ if (error?.code === "ENOENT") {
41
+ return Object.freeze({
42
+ contents: "",
43
+ exists: false
44
+ });
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ async function restoreProjectFiles(updates, packageLockPath, packageLockSnapshot) {
51
+ const rollbackUpdates = updates.map((update) => Object.freeze({
52
+ contents: update.manifest.contents,
53
+ manifest: Object.freeze({
54
+ ...update.manifest,
55
+ contents: update.contents
56
+ })
57
+ }));
58
+ await writeManifestUpdates(rollbackUpdates);
59
+ if (packageLockSnapshot.exists) {
60
+ const temporaryPath = `${packageLockPath}.jskit-${process.pid}-rollback.tmp`;
61
+ try {
62
+ await writeFile(temporaryPath, packageLockSnapshot.contents, "utf8");
63
+ await rename(temporaryPath, packageLockPath);
64
+ } finally {
65
+ await rm(temporaryPath, { force: true });
66
+ }
67
+ return;
68
+ }
69
+ await rm(packageLockPath, { force: true });
70
+ }
71
+
72
+ async function spawnNpmInstall(projectRoot) {
73
+ await new Promise((resolve, reject) => {
74
+ const child = spawn("npm", ["install"], {
75
+ cwd: projectRoot,
76
+ env: process.env,
77
+ stdio: "inherit"
78
+ });
79
+ child.on("error", reject);
80
+ child.on("close", (code, signal) => {
81
+ if (code === 0) {
82
+ resolve();
83
+ return;
84
+ }
85
+ reject(new Error(`npm install failed (code=${code}, signal=${signal || "none"}).`));
86
+ });
87
+ });
88
+ }
89
+
90
+ async function runNpmInstall(projectRoot) {
91
+ const nodeModulesPath = path.join(projectRoot, "node_modules");
92
+ const hadNodeModules = await fileExists(nodeModulesPath);
93
+ const backupRoot = hadNodeModules
94
+ ? await mkdtemp(path.join(projectRoot, ".jskit-update-"))
95
+ : null;
96
+ // Preserve the dependency directory name so development watchers keep excluding it.
97
+ const backupPath = backupRoot ? path.join(backupRoot, "node_modules") : null;
98
+ if (hadNodeModules) {
99
+ try {
100
+ await rename(nodeModulesPath, backupPath);
101
+ } catch (error) {
102
+ await rm(backupRoot, { force: true, recursive: true });
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ let settled = false;
108
+ const transaction = Object.freeze({
109
+ async commit() {
110
+ if (settled) {
111
+ return;
112
+ }
113
+ if (backupRoot) {
114
+ await rm(backupRoot, { force: true, recursive: true });
115
+ }
116
+ settled = true;
117
+ },
118
+ async rollback() {
119
+ if (settled) {
120
+ return;
121
+ }
122
+ try {
123
+ await rm(nodeModulesPath, { force: true, recursive: true });
124
+ if (backupPath) {
125
+ await rename(backupPath, nodeModulesPath);
126
+ }
127
+ } catch (error) {
128
+ if (backupPath) {
129
+ throw new Error(`Could not restore node_modules; backup retained at ${backupPath}.`, { cause: error });
130
+ }
131
+ throw error;
132
+ }
133
+ if (backupRoot) {
134
+ await rm(backupRoot, { force: true, recursive: true });
135
+ }
136
+ settled = true;
137
+ }
138
+ });
139
+
140
+ try {
141
+ await spawnNpmInstall(projectRoot);
142
+ return transaction;
143
+ } catch (error) {
144
+ try {
145
+ await transaction.rollback();
146
+ } catch (rollbackError) {
147
+ throw new AggregateError(
148
+ [error, rollbackError],
149
+ `npm install failed and the previous node_modules directory could not be restored. ${rollbackError.message}`
150
+ );
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ export { readOptionalFile, restoreProjectFiles, runNpmInstall, writeManifestUpdates };
@@ -0,0 +1,133 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileExists, toPosixPath } from "./files.mjs";
4
+
5
+ // A JSKIT project's manifests: its root package.json and every workspace
6
+ // package.json it declares, read as records that keep their original text.
7
+ const SKIPPED_DIRECTORY_NAMES = new Set([
8
+ ".git",
9
+ ".cache",
10
+ ".next",
11
+ ".nuxt",
12
+ ".output",
13
+ ".turbo",
14
+ "build",
15
+ "coverage",
16
+ "dist",
17
+ "node_modules",
18
+ "tmp"
19
+ ]);
20
+ function normalizeWorkspacePatterns(value) {
21
+ if (Array.isArray(value)) {
22
+ return value;
23
+ }
24
+ if (value && typeof value === "object" && Array.isArray(value.packages)) {
25
+ return value.packages;
26
+ }
27
+ return [];
28
+ }
29
+
30
+ function globToRegExp(pattern = "") {
31
+ const source = toPosixPath(pattern).replace(/^\.\//u, "").replace(/\/$/u, "");
32
+ let expression = "^";
33
+
34
+ for (let index = 0; index < source.length; index += 1) {
35
+ const character = source[index];
36
+ if (character === "*") {
37
+ if (source[index + 1] === "*") {
38
+ index += 1;
39
+ if (source[index + 1] === "/") {
40
+ index += 1;
41
+ expression += "(?:.*/)?";
42
+ } else {
43
+ expression += ".*";
44
+ }
45
+ } else {
46
+ expression += "[^/]*";
47
+ }
48
+ continue;
49
+ }
50
+ if (character === "?") {
51
+ expression += "[^/]";
52
+ continue;
53
+ }
54
+ expression += /[.+^${}()|[\]\\]/u.test(character) ? `\\${character}` : character;
55
+ }
56
+
57
+ return new RegExp(`${expression}$`, "u");
58
+ }
59
+
60
+ function createWorkspaceMatcher(workspaces) {
61
+ const patterns = normalizeWorkspacePatterns(workspaces)
62
+ .map((entry) => String(entry || "").trim())
63
+ .filter(Boolean);
64
+ const includes = patterns
65
+ .filter((entry) => !entry.startsWith("!"))
66
+ .map(globToRegExp);
67
+ const excludes = patterns
68
+ .filter((entry) => entry.startsWith("!"))
69
+ .map((entry) => globToRegExp(entry.slice(1)));
70
+
71
+ return function matchesWorkspace(relativeDirectory = "") {
72
+ const normalized = toPosixPath(relativeDirectory).replace(/^\.\//u, "").replace(/\/$/u, "");
73
+ return includes.some((matcher) => matcher.test(normalized)) &&
74
+ !excludes.some((matcher) => matcher.test(normalized));
75
+ };
76
+ }
77
+
78
+ async function readJsonRecord(absolutePath) {
79
+ const contents = await readFile(absolutePath, "utf8");
80
+ let value;
81
+ try {
82
+ value = JSON.parse(contents);
83
+ } catch (error) {
84
+ throw new Error(`${absolutePath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
85
+ }
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
87
+ throw new TypeError(`${absolutePath} must contain a JSON object.`);
88
+ }
89
+ return { absolutePath, contents, value };
90
+ }
91
+
92
+ async function collectNestedPackageJsonPaths(directory, results = []) {
93
+ for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
94
+ if (!entry.isDirectory() || SKIPPED_DIRECTORY_NAMES.has(entry.name) || entry.name.startsWith(".")) {
95
+ continue;
96
+ }
97
+ const childDirectory = path.join(directory, entry.name);
98
+ const manifestPath = path.join(childDirectory, "package.json");
99
+ if (await fileExists(manifestPath)) {
100
+ results.push(manifestPath);
101
+ }
102
+ await collectNestedPackageJsonPaths(childDirectory, results);
103
+ }
104
+ return results;
105
+ }
106
+
107
+ async function discoverProjectManifests(projectRoot, additionalManifests = []) {
108
+ const root = path.resolve(projectRoot);
109
+ const rootManifestPath = path.join(root, "package.json");
110
+ if (!(await fileExists(rootManifestPath))) {
111
+ throw new Error(`JSKIT project root has no package.json: ${root}`);
112
+ }
113
+ const rootManifest = await readJsonRecord(rootManifestPath);
114
+ const matchesWorkspace = createWorkspaceMatcher(rootManifest.value.workspaces);
115
+ const nestedPaths = await collectNestedPackageJsonPaths(root);
116
+ const workspacePaths = nestedPaths
117
+ .filter((manifestPath) => matchesWorkspace(path.relative(root, path.dirname(manifestPath))))
118
+ .sort((left, right) => left.localeCompare(right));
119
+ const manifests = [rootManifest];
120
+ for (const manifestPath of new Set([...workspacePaths, ...additionalManifests.map((file) => path.resolve(root, file))])) {
121
+ if (manifestPath === rootManifestPath) continue;
122
+ manifests.push(await readJsonRecord(manifestPath));
123
+ }
124
+ return Object.freeze(manifests);
125
+ }
126
+
127
+ function serializePackageJson(value, originalContents = "") {
128
+ const indentMatch = /\n([ \t]+)"/u.exec(String(originalContents || ""));
129
+ const indentation = indentMatch?.[1] || " ";
130
+ return `${JSON.stringify(value, null, indentation)}\n`;
131
+ }
132
+
133
+ export { discoverProjectManifests, readJsonRecord, serializePackageJson };