@jxsuite/schema 4.1.0 → 5.1.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/defs/field-schema.schema.ts +37 -4
- package/package.json +2 -1
- package/schema.json +38 -0
- package/schemas/document.paths.schema.json +30 -0
- package/src/component-meta.ts +2 -2
- package/src/validate-project.ts +24 -17
- package/src/which.ts +75 -0
- package/types.ts +6 -4
|
@@ -62,10 +62,12 @@ export const relationshipRefSchema = {
|
|
|
62
62
|
* ones every per-project entry document unions extension shapes on top of.
|
|
63
63
|
*
|
|
64
64
|
* These mirror `resolvePathEntries` in the compiler's pages-discovery exactly (specs/
|
|
65
|
-
* site-architecture.md §4.3): an explicit value list, a JSON data-file reference,
|
|
66
|
-
* bare array of parameter objects. Anything else is dispatched by discriminator key to
|
|
67
|
-
* extension's `resolvePaths` capability, so extension shapes reach the union through the entry
|
|
68
|
-
* document rather than from here.
|
|
65
|
+
* site-architecture.md §4.3): an explicit value list, a JSON data-file reference, a paged list, and
|
|
66
|
+
* the legacy bare array of parameter objects. Anything else is dispatched by discriminator key to
|
|
67
|
+
* an extension's `resolvePaths` capability, so extension shapes reach the union through the entry
|
|
68
|
+
* document rather than from here. The paged list is expanded by `expandDynamicRoutes` itself rather
|
|
69
|
+
* than `resolvePathEntries`, because it reads the page's own state; its `each` holds another source
|
|
70
|
+
* of this union (or an extension's), checked when the route expands.
|
|
69
71
|
*
|
|
70
72
|
* Each shape is CLOSED (`additionalProperties: false`). `$paths` is a build-time instruction with a
|
|
71
73
|
* fully specified grammar and no forward-compatible extension points of its own — a stray or
|
|
@@ -103,6 +105,37 @@ export const documentPathsCoreMembers = [
|
|
|
103
105
|
required: ["$ref"],
|
|
104
106
|
type: "object",
|
|
105
107
|
},
|
|
108
|
+
{
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
description:
|
|
111
|
+
"Paged list: one route per `pageSize` items of an entry of this page's own state, which must " +
|
|
112
|
+
"resolve to an array at build time. The page number fills the route's one parameter that " +
|
|
113
|
+
"`each` does not.",
|
|
114
|
+
properties: {
|
|
115
|
+
each: {
|
|
116
|
+
description:
|
|
117
|
+
"Another `$paths` source for the route's other parameter: the list is paged once per " +
|
|
118
|
+
"value of it, as a paged archive per tag is.",
|
|
119
|
+
type: ["object", "array"],
|
|
120
|
+
},
|
|
121
|
+
pageSize: { description: "Items on each page.", minimum: 1, type: "integer" },
|
|
122
|
+
paginate: {
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
description: "The state entry to page.",
|
|
125
|
+
properties: {
|
|
126
|
+
$ref: {
|
|
127
|
+
description: "A pointer to an entry of this page's state: `#/state/<key>`.",
|
|
128
|
+
pattern: "^#/state/[^/]+$",
|
|
129
|
+
type: "string",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ["$ref"],
|
|
133
|
+
type: "object",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: ["paginate", "pageSize"],
|
|
137
|
+
type: "object",
|
|
138
|
+
},
|
|
106
139
|
{
|
|
107
140
|
description:
|
|
108
141
|
"Legacy explicit form: an array of ready-made parameter objects, used as-is with no " +
|
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": "
|
|
64
|
+
"version": "5.1.0",
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"json-schema-to-ts": "^3.1.1"
|
|
66
67
|
}
|
package/schema.json
CHANGED
|
@@ -28351,6 +28351,44 @@
|
|
|
28351
28351
|
],
|
|
28352
28352
|
"type": "object"
|
|
28353
28353
|
},
|
|
28354
|
+
{
|
|
28355
|
+
"additionalProperties": false,
|
|
28356
|
+
"description": "Paged list: one route per `pageSize` items of an entry of this page's own state, which must resolve to an array at build time. The page number fills the route's one parameter that `each` does not.",
|
|
28357
|
+
"properties": {
|
|
28358
|
+
"each": {
|
|
28359
|
+
"description": "Another `$paths` source for the route's other parameter: the list is paged once per value of it, as a paged archive per tag is.",
|
|
28360
|
+
"type": [
|
|
28361
|
+
"object",
|
|
28362
|
+
"array"
|
|
28363
|
+
]
|
|
28364
|
+
},
|
|
28365
|
+
"pageSize": {
|
|
28366
|
+
"description": "Items on each page.",
|
|
28367
|
+
"minimum": 1,
|
|
28368
|
+
"type": "integer"
|
|
28369
|
+
},
|
|
28370
|
+
"paginate": {
|
|
28371
|
+
"additionalProperties": false,
|
|
28372
|
+
"description": "The state entry to page.",
|
|
28373
|
+
"properties": {
|
|
28374
|
+
"$ref": {
|
|
28375
|
+
"description": "A pointer to an entry of this page's state: `#/state/<key>`.",
|
|
28376
|
+
"pattern": "^#/state/[^/]+$",
|
|
28377
|
+
"type": "string"
|
|
28378
|
+
}
|
|
28379
|
+
},
|
|
28380
|
+
"required": [
|
|
28381
|
+
"$ref"
|
|
28382
|
+
],
|
|
28383
|
+
"type": "object"
|
|
28384
|
+
}
|
|
28385
|
+
},
|
|
28386
|
+
"required": [
|
|
28387
|
+
"paginate",
|
|
28388
|
+
"pageSize"
|
|
28389
|
+
],
|
|
28390
|
+
"type": "object"
|
|
28391
|
+
},
|
|
28354
28392
|
{
|
|
28355
28393
|
"description": "Legacy explicit form: an array of ready-made parameter objects, used as-is with no expansion. Prefer one of the source shapes above.",
|
|
28356
28394
|
"items": {
|
|
@@ -38,6 +38,36 @@
|
|
|
38
38
|
"required": ["$ref"],
|
|
39
39
|
"type": "object"
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"description": "Paged list: one route per `pageSize` items of an entry of this page's own state, which must resolve to an array at build time. The page number fills the route's one parameter that `each` does not.",
|
|
44
|
+
"properties": {
|
|
45
|
+
"each": {
|
|
46
|
+
"description": "Another `$paths` source for the route's other parameter: the list is paged once per value of it, as a paged archive per tag is.",
|
|
47
|
+
"type": ["object", "array"]
|
|
48
|
+
},
|
|
49
|
+
"pageSize": {
|
|
50
|
+
"description": "Items on each page.",
|
|
51
|
+
"minimum": 1,
|
|
52
|
+
"type": "integer"
|
|
53
|
+
},
|
|
54
|
+
"paginate": {
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"description": "The state entry to page.",
|
|
57
|
+
"properties": {
|
|
58
|
+
"$ref": {
|
|
59
|
+
"description": "A pointer to an entry of this page's state: `#/state/<key>`.",
|
|
60
|
+
"pattern": "^#/state/[^/]+$",
|
|
61
|
+
"type": "string"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": ["$ref"],
|
|
65
|
+
"type": "object"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"required": ["paginate", "pageSize"],
|
|
69
|
+
"type": "object"
|
|
70
|
+
},
|
|
41
71
|
{
|
|
42
72
|
"description": "Legacy explicit form: an array of ready-made parameter objects, used as-is with no expansion. Prefer one of the source shapes above.",
|
|
43
73
|
"items": {
|
package/src/component-meta.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/src/validate-project.ts
CHANGED
|
@@ -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.
|
|
115
|
-
* under `<root>/node_modules/`
|
|
116
|
-
* (in-repo starters have no local node_modules
|
|
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
|
-
//
|
|
141
|
-
//
|
|
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 (
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
175
|
-
{ cause:
|
|
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
|
+
}
|
package/types.ts
CHANGED
|
@@ -793,14 +793,16 @@ export interface ProjectConfig {
|
|
|
793
793
|
export type JxAttributeValue = Bindable<string | number | boolean>;
|
|
794
794
|
|
|
795
795
|
/**
|
|
796
|
-
* A dynamic-route path source (spec §4.3): explicit values, data-file ref, a
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
796
|
+
* A dynamic-route path source (spec §4.3): explicit values, data-file ref, a paged list of an entry
|
|
797
|
+
* of the page's own state (with `each` naming the source of a second parameter), a legacy array of
|
|
798
|
+
* param objects, or an extension-discriminated source (an object carrying a `resolvePaths`
|
|
799
|
+
* discriminator key registered by an enabled extension, e.g. the parser's `contentType` — the
|
|
800
|
+
* extension owns the narrow shape).
|
|
800
801
|
*/
|
|
801
802
|
export type JxPathsDef =
|
|
802
803
|
| { values: JsonValue[]; param?: string }
|
|
803
804
|
| { $ref: string; param?: string; field?: string }
|
|
805
|
+
| { paginate: { $ref: string }; pageSize: number; each?: JsonValue }
|
|
804
806
|
| { param?: string; field?: string; [discriminator: string]: JsonValue | undefined }
|
|
805
807
|
| Record<string, JsonValue>[];
|
|
806
808
|
|