@jxsuite/schema 4.1.0 → 5.0.0

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
@@ -20,6 +20,7 @@
20
20
  "./locale": "./src/locale.ts",
21
21
  "./intl": "./src/intl.ts",
22
22
  "./media-type": "./src/media-type.ts",
23
+ "./which": "./src/which.ts",
23
24
  "./overlays": "./src/overlays.ts",
24
25
  "./dialogs": "./src/dialogs.ts",
25
26
  "./a11y": "./src/a11y.ts",
@@ -60,7 +61,7 @@
60
61
  "upgrade": "bun update --latest && bun install"
61
62
  },
62
63
  "type": "module",
63
- "version": "4.1.0",
64
+ "version": "5.0.0",
64
65
  "devDependencies": {
65
66
  "json-schema-to-ts": "^3.1.1"
66
67
  }
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * It lives here because THREE backends need the same answer and two of them had already written it
10
10
  * twice: `packages/server/src/studio-api.ts` (the dev server's `/__studio/components`) and
11
- * `packages/desktop/src/project-session.ts`. A third copy in the cloud adapter would have made
12
- * "which entries in `state` count as props" a question with three answers.
11
+ * `packages/desktop/src/backend/project-session.ts`. A third copy in the cloud adapter would have
12
+ * made "which entries in `state` count as props" a question with three answers.
13
13
  *
14
14
  * @license MIT
15
15
  */
@@ -111,9 +111,11 @@ export async function validateProjectFile(projectRoot: string): Promise<ProjectV
111
111
  }
112
112
 
113
113
  /**
114
- * Restricted schema loader: only `file:` URLs inside the project root are served. Missing files
115
- * under `<root>/node_modules/` fall back to host resolution of the corresponding bare specifier
116
- * (in-repo starters have no local node_modules; the fragments still ship with the host).
114
+ * Restricted schema loader: only `file:` URLs inside the project root are served. A missing file
115
+ * under `<root>/node_modules/` resolves its bare specifier through the project's own resolution
116
+ * (in-repo starters have no local node_modules and walk up to the workspace's). Only this package's
117
+ * own core schemas fall back to the host: an extension's fragments, first-party ones included, are
118
+ * the project's to supply (specs/extensions.md §5.4).
117
119
  *
118
120
  * @param {string} uri - The `$ref` target ajv could not resolve from already-loaded resources
119
121
  * @param {string} root - Absolute project root
@@ -137,8 +139,8 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<JsonValu
137
139
  return readJson(filePath);
138
140
  }
139
141
 
140
- // Host fallback: <root>/node_modules/<specifier> is absent — resolve the specifier through the
141
- // Project's own resolution first, then the host's (workspace symlinks resolve transparently).
142
+ // <root>/node_modules/<specifier> is absent — resolve the specifier through the project's own
143
+ // Resolution (workspace symlinks resolve transparently), and through the host only for the core.
142
144
  const posixRel = rel.split("\\").join("/");
143
145
  const marker = "node_modules/";
144
146
  const markerIndex = posixRel.lastIndexOf(marker);
@@ -147,32 +149,37 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<JsonValu
147
149
  }
148
150
  const specifier = posixRel.slice(markerIndex + marker.length);
149
151
 
152
+ const ownPrefix = "@jxsuite/schema/";
150
153
  let resolved: string;
151
154
  try {
152
155
  const projRequire = createRequire(resolve(root, "package.json"));
153
156
  resolved = projRequire.resolve(specifier);
154
- } catch {
157
+ } catch (error) {
158
+ if (!specifier.startsWith(ownPrefix)) {
159
+ throw new Error(
160
+ `Schema ref "${uri}" does not exist and "${specifier}" is not resolvable from the ` +
161
+ `project — is the package installed?`,
162
+ { cause: error },
163
+ );
164
+ }
155
165
  try {
156
166
  const selfRequire = createRequire(import.meta.url);
157
167
  resolved = selfRequire.resolve(specifier);
158
- } catch (error) {
168
+ } catch (hostError) {
159
169
  // Exports-map subpath resolution is brittle against version-skewed shadowing installs (a
160
170
  // Stale local copy terminates Node's walk-up with ERR_PACKAGE_PATH_NOT_EXPORTED). Our own
161
171
  // Schema files are pure data and the running validator's copy is guaranteed to match, so
162
172
  // Read them straight off this package's disk as the last resort.
163
- const ownPrefix = "@jxsuite/schema/";
164
- if (specifier.startsWith(ownPrefix)) {
165
- const ownFile = fileURLToPath(
166
- new URL(`../${specifier.slice(ownPrefix.length)}`, import.meta.url),
167
- );
168
- if (existsSync(ownFile)) {
169
- return readJson(ownFile);
170
- }
173
+ const ownFile = fileURLToPath(
174
+ new URL(`../${specifier.slice(ownPrefix.length)}`, import.meta.url),
175
+ );
176
+ if (existsSync(ownFile)) {
177
+ return readJson(ownFile);
171
178
  }
172
179
  throw new Error(
173
180
  `Schema ref "${uri}" does not exist and "${specifier}" is not resolvable from the ` +
174
- `project or the host`,
175
- { cause: error },
181
+ `project or this host's core`,
182
+ { cause: hostError },
176
183
  );
177
184
  }
178
185
  }
package/src/which.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Find an executable the way `Bun.which` does, on `node:fs` alone.
3
+ *
4
+ * The desktop backend runs on Electron's Node (specs/desktop.md §7.1), so the code it reaches
5
+ * cannot call `Bun.which`. The answer is held to Bun's by `tests/which.test.ts`: a name containing
6
+ * a path separator is resolved against the working directory and returned when it is an executable
7
+ * regular file; any other name is looked up in each non-empty `PATH` entry in order. On Windows,
8
+ * where every file passes an `X_OK` check, only a launchable name counts: one that already ends in
9
+ * `.exe`, `.cmd`, `.bat` or `.com` is tried as written, and any other gets each of those appended
10
+ * in that order. Like Bun, it ignores `PATHEXT`, whose `.js` and `.vbs` entries name files a
11
+ * process cannot spawn. The path returned is the one that was tried, so an appended extension is
12
+ * lower case whatever the file's own case, and never its realpath.
13
+ *
14
+ * Node-only: it reads the filesystem, unlike the rest of this package's browser-safe modules.
15
+ */
16
+ import { accessSync, constants, statSync } from "node:fs";
17
+ import { delimiter, join, resolve } from "node:path";
18
+
19
+ /** True when `path` is a regular file this process may execute. */
20
+ function isExecutable(path: string): boolean {
21
+ try {
22
+ if (!statSync(path).isFile()) {
23
+ return false;
24
+ }
25
+ accessSync(path, constants.X_OK);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /** The extensions Bun's `which` tries on Windows, in its order. */
33
+ const WINDOWS_EXTENSIONS = [".exe", ".cmd", ".bat", ".com"];
34
+
35
+ /**
36
+ * The names to try for `name`: itself elsewhere; on Windows, only names with a launchable
37
+ * extension.
38
+ */
39
+ function candidates(name: string, platform: NodeJS.Platform): string[] {
40
+ if (platform !== "win32") {
41
+ return [name];
42
+ }
43
+ const lower = name.toLowerCase();
44
+ if (WINDOWS_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
45
+ return [name];
46
+ }
47
+ return WINDOWS_EXTENSIONS.map((extension) => `${name}${extension}`);
48
+ }
49
+
50
+ /**
51
+ * The absolute path of the executable `name`, or null when there is none.
52
+ *
53
+ * @param name A command name, or a path (absolute, or relative to the working directory)
54
+ * @param options `PATH` overrides the environment's search path
55
+ */
56
+ export function which(
57
+ name: string,
58
+ options: { PATH?: string; platform?: NodeJS.Platform } = {},
59
+ ): string | null {
60
+ if (name === "") {
61
+ return null;
62
+ }
63
+ const platform = options.platform ?? process.platform;
64
+ if (name.includes("/") || (platform === "win32" && name.includes("\\"))) {
65
+ return candidates(resolve(name), platform).find((path) => isExecutable(path)) ?? null;
66
+ }
67
+ const dirs = (options.PATH ?? process.env.PATH ?? "").split(delimiter).filter(Boolean);
68
+ for (const dir of dirs) {
69
+ const found = candidates(join(dir, name), platform).find((path) => isExecutable(path));
70
+ if (found) {
71
+ return found;
72
+ }
73
+ }
74
+ return null;
75
+ }