@jxsuite/schema 1.1.0 → 1.3.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/LICENSE +21 -0
- package/class-schema.json +103 -1
- package/defs/children-value.schema.ts +10 -1
- package/defs/class-def.schema.ts +107 -1
- package/defs/element-def.schema.ts +15 -2
- package/defs/external-class-def.schema.ts +14 -1
- package/defs/function-def.schema.ts +8 -0
- package/defs/index.ts +4 -0
- package/defs/project-config.schema.ts +10 -3
- package/defs/style-object.schema.ts +4 -10
- package/package.json +2 -1
- package/project-schema.json +22 -3
- package/schema.json +1053 -20
- package/schemas/document.paths.schema.json +1 -1
- package/schemas/project.core.schema.json +40 -79
- package/schemas/project.fields.schema.json +1 -1
- package/src/asset-paths.ts +42 -0
- package/src/extension-registry.ts +5 -0
- package/src/format-registry.ts +5 -2
- package/src/guards.ts +10 -0
- package/src/schema.ts +69 -4
- package/src/validate-project.ts +13 -0
|
@@ -41,10 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"service": {
|
|
43
43
|
"description": "How images are optimized: \"build\" runs Sharp at build time; \"cloudflare\" emits /cdn-cgi/image transform URLs served by Cloudflare Image Transformations (requires the feature to be enabled on the serving zone).",
|
|
44
|
-
"enum": [
|
|
45
|
-
"build",
|
|
46
|
-
"cloudflare"
|
|
47
|
-
],
|
|
44
|
+
"enum": ["build", "cloudflare"],
|
|
48
45
|
"type": "string"
|
|
49
46
|
},
|
|
50
47
|
"sizes": {
|
|
@@ -88,20 +85,11 @@
|
|
|
88
85
|
"type": "array"
|
|
89
86
|
},
|
|
90
87
|
"type": {
|
|
91
|
-
"enum": [
|
|
92
|
-
"string",
|
|
93
|
-
"number",
|
|
94
|
-
"integer",
|
|
95
|
-
"boolean",
|
|
96
|
-
"array",
|
|
97
|
-
"object"
|
|
98
|
-
],
|
|
88
|
+
"enum": ["string", "number", "integer", "boolean", "array", "object"],
|
|
99
89
|
"type": "string"
|
|
100
90
|
}
|
|
101
91
|
},
|
|
102
|
-
"required": [
|
|
103
|
-
"type"
|
|
104
|
-
],
|
|
92
|
+
"required": ["type"],
|
|
105
93
|
"type": "object"
|
|
106
94
|
},
|
|
107
95
|
"RelationshipRef": {
|
|
@@ -115,9 +103,25 @@
|
|
|
115
103
|
"type": "string"
|
|
116
104
|
}
|
|
117
105
|
},
|
|
118
|
-
"required": [
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
"required": ["$ref"],
|
|
107
|
+
"type": "object"
|
|
108
|
+
},
|
|
109
|
+
"StyleObject": {
|
|
110
|
+
"additionalProperties": {
|
|
111
|
+
"oneOf": [
|
|
112
|
+
{
|
|
113
|
+
"type": "string"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"type": "number"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"$ref": "#/$defs/StyleObject"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"description": "CSS style definition. camelCase property names follow CSSOM convention. Keys starting with :, ., &, or [ are treated as nested CSS selectors. Keys matching $media breakpoint names (or other @-prefixed at-rules) group nested rules. Nesting is recursive: selector and at-rule groups may nest to arbitrary depth (e.g. breakpoint → selector → pseudo-class), mirroring the compiler's recursive emission.",
|
|
124
|
+
"properties": {},
|
|
121
125
|
"type": "object"
|
|
122
126
|
}
|
|
123
127
|
},
|
|
@@ -140,9 +144,7 @@
|
|
|
140
144
|
"type": "string"
|
|
141
145
|
}
|
|
142
146
|
},
|
|
143
|
-
"required": [
|
|
144
|
-
"$ref"
|
|
145
|
-
],
|
|
147
|
+
"required": ["$ref"],
|
|
146
148
|
"type": "object"
|
|
147
149
|
},
|
|
148
150
|
{
|
|
@@ -192,9 +194,10 @@
|
|
|
192
194
|
"additionalProperties": {
|
|
193
195
|
"type": "string"
|
|
194
196
|
},
|
|
195
|
-
"description": "Named media breakpoints following CSS @custom-media convention. Available in all component style objects.",
|
|
197
|
+
"description": "Named media breakpoints following CSS @custom-media convention. Available in all component style objects. Declaring a pure color-scheme query (e.g. \"--dark\": \"(prefers-color-scheme: dark)\") additionally opts the site into the forced-scheme contract: @--dark style blocks are dual-emitted so a data-color-scheme attribute on <html> overrides the OS preference, color-scheme is declared on :root, and a pre-paint script restores the visitor's persisted choice (spec §9.5).",
|
|
196
198
|
"examples": [
|
|
197
199
|
{
|
|
200
|
+
"--dark": "(prefers-color-scheme: dark)",
|
|
198
201
|
"--lg": "(min-width: 1024px)",
|
|
199
202
|
"--md": "(min-width: 768px)",
|
|
200
203
|
"--sm": "(min-width: 640px)"
|
|
@@ -211,13 +214,7 @@
|
|
|
211
214
|
"properties": {
|
|
212
215
|
"adapter": {
|
|
213
216
|
"description": "Platform adapter for deployment-specific output.",
|
|
214
|
-
"enum": [
|
|
215
|
-
"static",
|
|
216
|
-
"cloudflare-pages",
|
|
217
|
-
"cloudflare-workers",
|
|
218
|
-
"node",
|
|
219
|
-
"bun"
|
|
220
|
-
],
|
|
217
|
+
"enum": ["static", "cloudflare-pages", "cloudflare-workers", "node", "bun"],
|
|
221
218
|
"type": "string"
|
|
222
219
|
},
|
|
223
220
|
"deploy": {
|
|
@@ -239,26 +236,17 @@
|
|
|
239
236
|
},
|
|
240
237
|
"provider": {
|
|
241
238
|
"description": "Hosting provider the project is connected to.",
|
|
242
|
-
"enum": [
|
|
243
|
-
"cloudflare-pages"
|
|
244
|
-
],
|
|
239
|
+
"enum": ["cloudflare-pages"],
|
|
245
240
|
"type": "string"
|
|
246
241
|
}
|
|
247
242
|
},
|
|
248
|
-
"required": [
|
|
249
|
-
"provider",
|
|
250
|
-
"accountId",
|
|
251
|
-
"projectName"
|
|
252
|
-
],
|
|
243
|
+
"required": ["provider", "accountId", "projectName"],
|
|
253
244
|
"type": "object"
|
|
254
245
|
},
|
|
255
246
|
"format": {
|
|
256
247
|
"default": "directory",
|
|
257
248
|
"description": "Reserved; currently unused. Accepted for forward compatibility with single-file output (desktop single-file mode) — the build emits per-route directories regardless.",
|
|
258
|
-
"enum": [
|
|
259
|
-
"directory",
|
|
260
|
-
"single"
|
|
261
|
-
],
|
|
249
|
+
"enum": ["directory", "single"],
|
|
262
250
|
"type": "string"
|
|
263
251
|
},
|
|
264
252
|
"outDir": {
|
|
@@ -274,11 +262,7 @@
|
|
|
274
262
|
"trailingSlash": {
|
|
275
263
|
"default": "always",
|
|
276
264
|
"description": "Trailing slash behavior for generated URLs.",
|
|
277
|
-
"enum": [
|
|
278
|
-
"always",
|
|
279
|
-
"never",
|
|
280
|
-
"ignore"
|
|
281
|
-
],
|
|
265
|
+
"enum": ["always", "never", "ignore"],
|
|
282
266
|
"type": "string"
|
|
283
267
|
}
|
|
284
268
|
},
|
|
@@ -307,9 +291,7 @@
|
|
|
307
291
|
"layout": {
|
|
308
292
|
"default": null,
|
|
309
293
|
"description": "Default layout file path applied to all pages. Set to null to render pages without a layout.",
|
|
310
|
-
"examples": [
|
|
311
|
-
"./layouts/base.json"
|
|
312
|
-
],
|
|
294
|
+
"examples": ["./layouts/base.json"],
|
|
313
295
|
"oneOf": [
|
|
314
296
|
{
|
|
315
297
|
"type": "string"
|
|
@@ -324,11 +306,7 @@
|
|
|
324
306
|
},
|
|
325
307
|
"extensions": {
|
|
326
308
|
"description": "Extension packages: bare package names (resolved project-first through the package exports map) or relative paths. Each must export jx-extension.json.",
|
|
327
|
-
"examples": [
|
|
328
|
-
[
|
|
329
|
-
"@jxsuite/parser"
|
|
330
|
-
]
|
|
331
|
-
],
|
|
309
|
+
"examples": [["@jxsuite/parser"]],
|
|
332
310
|
"items": {
|
|
333
311
|
"type": "string"
|
|
334
312
|
},
|
|
@@ -339,20 +317,12 @@
|
|
|
339
317
|
"properties": {
|
|
340
318
|
"defaultLocale": {
|
|
341
319
|
"description": "Default locale code.",
|
|
342
|
-
"examples": [
|
|
343
|
-
"en"
|
|
344
|
-
],
|
|
320
|
+
"examples": ["en"],
|
|
345
321
|
"type": "string"
|
|
346
322
|
},
|
|
347
323
|
"locales": {
|
|
348
324
|
"description": "Available locale codes.",
|
|
349
|
-
"examples": [
|
|
350
|
-
[
|
|
351
|
-
"en",
|
|
352
|
-
"fr",
|
|
353
|
-
"de"
|
|
354
|
-
]
|
|
355
|
-
],
|
|
325
|
+
"examples": [["en", "fr", "de"]],
|
|
356
326
|
"items": {
|
|
357
327
|
"type": "string"
|
|
358
328
|
},
|
|
@@ -360,10 +330,7 @@
|
|
|
360
330
|
},
|
|
361
331
|
"routing": {
|
|
362
332
|
"description": "Locale routing strategy.",
|
|
363
|
-
"enum": [
|
|
364
|
-
"prefix-except-default",
|
|
365
|
-
"prefix-always"
|
|
366
|
-
],
|
|
333
|
+
"enum": ["prefix-except-default", "prefix-always"],
|
|
367
334
|
"type": "string"
|
|
368
335
|
}
|
|
369
336
|
},
|
|
@@ -388,10 +355,7 @@
|
|
|
388
355
|
"name": {
|
|
389
356
|
"default": "Jx Site",
|
|
390
357
|
"description": "Human-readable project name.",
|
|
391
|
-
"examples": [
|
|
392
|
-
"My Portfolio",
|
|
393
|
-
"Jx Example Site"
|
|
394
|
-
],
|
|
358
|
+
"examples": ["My Portfolio", "Jx Example Site"],
|
|
395
359
|
"type": "string"
|
|
396
360
|
},
|
|
397
361
|
"redirects": {
|
|
@@ -420,22 +384,19 @@
|
|
|
420
384
|
"type": "number"
|
|
421
385
|
},
|
|
422
386
|
{
|
|
423
|
-
"
|
|
387
|
+
"$ref": "#/$defs/StyleObject"
|
|
424
388
|
}
|
|
425
389
|
]
|
|
426
390
|
},
|
|
427
|
-
"description": "Global CSS styles applied to the <body> element. Uses the same camelCase CSSOM convention as component styles.",
|
|
391
|
+
"description": "Global CSS styles applied to the <body> element. Uses the same camelCase CSSOM convention and recursive selector/at-rule nesting as component styles.",
|
|
428
392
|
"type": "object"
|
|
429
393
|
},
|
|
430
394
|
"url": {
|
|
431
395
|
"description": "Production URL of the deployed site.",
|
|
432
|
-
"examples": [
|
|
433
|
-
"https://example.com",
|
|
434
|
-
"https://jxsuite.com"
|
|
435
|
-
],
|
|
396
|
+
"examples": ["https://example.com", "https://jxsuite.com"],
|
|
436
397
|
"type": "string"
|
|
437
398
|
}
|
|
438
399
|
},
|
|
439
400
|
"title": "Jx Project Core",
|
|
440
401
|
"type": "object"
|
|
441
|
-
}
|
|
402
|
+
}
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
],
|
|
12
12
|
"description": "Default field-schema union for section entry schemas: the core field shape plus relationship references. Per-project entry documents override this resource with the effective union. See specs/extensions.md §5.3.",
|
|
13
13
|
"title": "Jx Project Field Union"
|
|
14
|
-
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic mapping from Function-def `$src` module specifiers to bundled asset URLs. Shared
|
|
3
|
+
* between the compiler's sidecar bundler (which writes the bundle) and extension `lower()`
|
|
4
|
+
* implementations (which emit the URL into client code) so neither needs to depend on the other.
|
|
5
|
+
*
|
|
6
|
+
* The mapping is intentionally hash-free: the same specifier always yields the same URL, letting
|
|
7
|
+
* lowered defs reference bundles the compiler produces later in the build. Distinct specifiers that
|
|
8
|
+
* collide on a slug are rejected by the bundler at build time.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Prefix for npm package specifiers in `$src` (spec §12): `npm:<pkg>[/subpath]`. */
|
|
12
|
+
export const NPM_SPECIFIER_PREFIX = "npm:";
|
|
13
|
+
|
|
14
|
+
/** URL directory (under the site root) where bundled sidecars are written. */
|
|
15
|
+
export const SIDECAR_ASSET_DIR = "/assets/";
|
|
16
|
+
|
|
17
|
+
/** True when a `$src` specifier names an npm package rather than a project file. */
|
|
18
|
+
export function isNpmSpecifier(specifier: string): boolean {
|
|
19
|
+
return specifier.startsWith(NPM_SPECIFIER_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map a Function-def `$src` specifier to its bundled asset URL.
|
|
24
|
+
*
|
|
25
|
+
* `npm:@jxsuite/search/client` → `/assets/jxsuite-search-client.js` `./lib/search-helpers.ts` →
|
|
26
|
+
* `/assets/lib-search-helpers.js`
|
|
27
|
+
*/
|
|
28
|
+
export function sidecarAssetPath(specifier: string): string {
|
|
29
|
+
let base = specifier;
|
|
30
|
+
if (isNpmSpecifier(base)) {
|
|
31
|
+
base = base.slice(NPM_SPECIFIER_PREFIX.length);
|
|
32
|
+
}
|
|
33
|
+
base = base
|
|
34
|
+
.replace(/^\.\//, "")
|
|
35
|
+
.replace(/\.(js|ts|mjs|mts)$/, "")
|
|
36
|
+
.replaceAll("@", "")
|
|
37
|
+
.replaceAll(/[/\\]+/g, "-")
|
|
38
|
+
.replaceAll(/[^\w.-]/g, "-")
|
|
39
|
+
.replaceAll(/-+/g, "-")
|
|
40
|
+
.replaceAll(/^-|-$/g, "");
|
|
41
|
+
return `${SIDECAR_ASSET_DIR}${base}.js`;
|
|
42
|
+
}
|
|
@@ -151,6 +151,11 @@ export class ExtensionRegistry {
|
|
|
151
151
|
return this.#classes.find((e) => e.capabilities.resolvePaths?.discriminator === key);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** Classes declaring an `emit` capability (build-time asset emission), in declaration order. */
|
|
155
|
+
emitters(): FormatEntry[] {
|
|
156
|
+
return this.#classes.filter((e) => e.capabilities.emit !== undefined);
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
/** Server-mount classes, sorted by `server.order` (ascending; default 100), then name. */
|
|
155
160
|
serverMounts(): FormatEntry[] {
|
|
156
161
|
return this.#classes
|
package/src/format-registry.ts
CHANGED
|
@@ -21,14 +21,16 @@ export const FORMAT_CAPABILITIES: readonly FormatCapability[] = [
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* All well-known static capability roles across admission blocks (specs/extensions.md §8): format
|
|
24
|
-
* roles plus project-section (`projectData`, `resolvePaths`), compile-away (`lower`),
|
|
25
|
-
* (`mount`), and connector (`dialect`, `deploySchema`,
|
|
24
|
+
* roles plus project-section (`projectData`, `resolvePaths`), compile-away (`lower`), build-time
|
|
25
|
+
* asset emission (`emit`), server-mount (`mount`), and connector (`dialect`, `deploySchema`,
|
|
26
|
+
* `bindings`, `testConnection`) roles.
|
|
26
27
|
*/
|
|
27
28
|
export type ExtensionCapability =
|
|
28
29
|
| FormatCapability
|
|
29
30
|
| "projectData"
|
|
30
31
|
| "resolvePaths"
|
|
31
32
|
| "lower"
|
|
33
|
+
| "emit"
|
|
32
34
|
| "mount"
|
|
33
35
|
| "dialect"
|
|
34
36
|
| "deploySchema"
|
|
@@ -40,6 +42,7 @@ export const EXTENSION_CAPABILITIES: readonly ExtensionCapability[] = [
|
|
|
40
42
|
"projectData",
|
|
41
43
|
"resolvePaths",
|
|
42
44
|
"lower",
|
|
45
|
+
"emit",
|
|
43
46
|
"mount",
|
|
44
47
|
"dialect",
|
|
45
48
|
"deploySchema",
|
package/src/guards.ts
CHANGED
|
@@ -174,6 +174,16 @@ export function isTemplateString(value?: unknown): value is string {
|
|
|
174
174
|
return typeof value === "string" && value.includes("${");
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Whether a function `body` returns a value — the heuristic that classifies a `$prototype:
|
|
179
|
+
* "Function"` as a computed value vs an event handler. Uses a word boundary so identifiers like
|
|
180
|
+
* `returned` do not match. Shared by the runtime interpreter and the compiler so both agree on the
|
|
181
|
+
* classification.
|
|
182
|
+
*/
|
|
183
|
+
export function bodyReturnsValue(body: string): boolean {
|
|
184
|
+
return /\breturn\b/.test(body);
|
|
185
|
+
}
|
|
186
|
+
|
|
177
187
|
// ─── Style guards & accessors ───────────────────────────────────────────────────
|
|
178
188
|
|
|
179
189
|
/** A nested style object (selector/media block), as opposed to a scalar CSS value. */
|
package/src/schema.ts
CHANGED
|
@@ -92,7 +92,10 @@ import {
|
|
|
92
92
|
classFieldDefSchema,
|
|
93
93
|
classMethodDefSchema,
|
|
94
94
|
classParameterDefSchema,
|
|
95
|
+
connectorBlockDefSchema,
|
|
95
96
|
formatDefSchema,
|
|
97
|
+
projectBlockDefSchema,
|
|
98
|
+
serverBlockDefSchema,
|
|
96
99
|
studioHintsSchema,
|
|
97
100
|
} from "../defs/class-def.schema";
|
|
98
101
|
import { extensionManifestSchema } from "../defs/extension-manifest.schema";
|
|
@@ -182,7 +185,11 @@ function buildEventHandlerProperties(eventHandlers: string[]) {
|
|
|
182
185
|
for (const name of eventHandlers) {
|
|
183
186
|
properties[name] = {
|
|
184
187
|
description: `Event handler for the "${name.slice(2)}" event.`,
|
|
185
|
-
oneOf: [
|
|
188
|
+
oneOf: [
|
|
189
|
+
{ $ref: "#/$defs/RefObject" },
|
|
190
|
+
{ $ref: "#/$defs/ExpressionEntry" },
|
|
191
|
+
{ $ref: "#/$defs/FunctionDef" },
|
|
192
|
+
],
|
|
186
193
|
};
|
|
187
194
|
}
|
|
188
195
|
return properties;
|
|
@@ -216,6 +223,9 @@ export async function generateSchema() {
|
|
|
216
223
|
ClassConstructorDef: classConstructorDefSchema,
|
|
217
224
|
ClassMethodDef: classMethodDefSchema,
|
|
218
225
|
FormatDef: formatDefSchema,
|
|
226
|
+
ProjectBlockDef: projectBlockDefSchema,
|
|
227
|
+
ServerBlockDef: serverBlockDefSchema,
|
|
228
|
+
ConnectorBlockDef: connectorBlockDefSchema,
|
|
219
229
|
StudioHints: studioHintsSchema,
|
|
220
230
|
ExternalClassDef: externalClassDefSchema,
|
|
221
231
|
ExpressionPointer: expressionPointerSchema,
|
|
@@ -338,7 +348,10 @@ export async function generateSchema() {
|
|
|
338
348
|
additionalProperties: { type: "string" },
|
|
339
349
|
description:
|
|
340
350
|
"Named media breakpoints following CSS @custom-media convention. " +
|
|
341
|
-
"Keys use the CSS custom property -- prefix."
|
|
351
|
+
"Keys use the CSS custom property -- prefix. Pure color-scheme queries " +
|
|
352
|
+
'(e.g. "--dark": "(prefers-color-scheme: dark)") are dual-emitted so a ' +
|
|
353
|
+
"data-color-scheme attribute on the root element forces the scheme " +
|
|
354
|
+
"regardless of the OS preference (spec §9.5).",
|
|
342
355
|
examples: [
|
|
343
356
|
{
|
|
344
357
|
"--dark": "(prefers-color-scheme: dark)",
|
|
@@ -412,6 +425,7 @@ export function generateProjectSchema() {
|
|
|
412
425
|
return {
|
|
413
426
|
$defs: {
|
|
414
427
|
ImageConfig: imageConfigSchema,
|
|
428
|
+
StyleObject: styleObjectSchema,
|
|
415
429
|
},
|
|
416
430
|
$id: "https://jxsuite.com/schema/project/v1",
|
|
417
431
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -443,6 +457,7 @@ export function generateProjectCoreSchema() {
|
|
|
443
457
|
ImageConfig: imageConfigSchema,
|
|
444
458
|
JxFieldSchema: jxFieldSchemaDef,
|
|
445
459
|
RelationshipRef: relationshipRefSchema,
|
|
460
|
+
StyleObject: styleObjectSchema,
|
|
446
461
|
},
|
|
447
462
|
$id: "https://jxsuite.com/schema/project/core/v2",
|
|
448
463
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -511,7 +526,10 @@ export function generateClassSchema() {
|
|
|
511
526
|
ClassFieldDef: classFieldDefSchema,
|
|
512
527
|
ClassMethodDef: classMethodDefSchema,
|
|
513
528
|
ClassParameterDef: classParameterDefSchema,
|
|
529
|
+
ConnectorBlockDef: connectorBlockDefSchema,
|
|
514
530
|
FormatDef: formatDefSchema,
|
|
531
|
+
ProjectBlockDef: projectBlockDefSchema,
|
|
532
|
+
ServerBlockDef: serverBlockDefSchema,
|
|
515
533
|
StudioHints: studioHintsSchema,
|
|
516
534
|
},
|
|
517
535
|
$id: "https://jxsuite.com/schema/class/v1",
|
|
@@ -562,6 +580,7 @@ export function generateClassSchema() {
|
|
|
562
580
|
},
|
|
563
581
|
$schema: { type: "string" },
|
|
564
582
|
$studio: { $ref: "#/$defs/StudioHints" },
|
|
583
|
+
connector: { $ref: "#/$defs/ConnectorBlockDef" },
|
|
565
584
|
description: { type: "string" },
|
|
566
585
|
extends: {
|
|
567
586
|
description: "Base class — string name or $ref to another .class.json.",
|
|
@@ -576,6 +595,8 @@ export function generateClassSchema() {
|
|
|
576
595
|
],
|
|
577
596
|
},
|
|
578
597
|
format: { $ref: "#/$defs/FormatDef" },
|
|
598
|
+
project: { $ref: "#/$defs/ProjectBlockDef" },
|
|
599
|
+
server: { $ref: "#/$defs/ServerBlockDef" },
|
|
579
600
|
title: {
|
|
580
601
|
description: "PascalCase class name, used as the export name.",
|
|
581
602
|
examples: ["MarkdownFile", "DataSource", "Calculator"],
|
|
@@ -610,7 +631,19 @@ type AjvCtor = new (opts: {
|
|
|
610
631
|
}) => AjvInstance;
|
|
611
632
|
type AddFormatsFn = (ajv: AjvInstance) => void;
|
|
612
633
|
|
|
613
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Validate a document against an arbitrary JSON Schema 2020-12 document (e.g. a bundled per-project
|
|
636
|
+
* document.schema.json from `jx schema`). The schema must be self-contained — external refs are not
|
|
637
|
+
* resolved here.
|
|
638
|
+
*
|
|
639
|
+
* @param {Record<string, unknown>} doc - The document to validate
|
|
640
|
+
* @param {Record<string, unknown>} schema - A self-contained 2020-12 schema
|
|
641
|
+
* @returns {Promise<{ errors: unknown[] | null; valid: boolean }>}
|
|
642
|
+
*/
|
|
643
|
+
export async function validateWithSchema(
|
|
644
|
+
doc: Record<string, unknown>,
|
|
645
|
+
schema: Record<string, unknown>,
|
|
646
|
+
) {
|
|
614
647
|
let Ajv: AjvCtor, addFormats: AddFormatsFn;
|
|
615
648
|
try {
|
|
616
649
|
// The generated schema is JSON Schema 2020-12, so use the matching Ajv build
|
|
@@ -625,13 +658,21 @@ export async function validateDocument(doc: Record<string, unknown>) {
|
|
|
625
658
|
const ajv = new Ajv({ allErrors: true, ownProperties: true, strict: false });
|
|
626
659
|
addFormats(ajv);
|
|
627
660
|
|
|
628
|
-
const schema = await generateSchema();
|
|
629
661
|
const validate = ajv.compile(schema);
|
|
630
662
|
const valid = validate(doc);
|
|
631
663
|
|
|
632
664
|
return { errors: validate.errors ?? null, valid };
|
|
633
665
|
}
|
|
634
666
|
|
|
667
|
+
export async function validateDocument(doc: Record<string, unknown>) {
|
|
668
|
+
return validateWithSchema(doc, (await generateSchema()) as Record<string, unknown>);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Validate a .class.json class definition against the generated class schema. */
|
|
672
|
+
export async function validateClass(doc: Record<string, unknown>) {
|
|
673
|
+
return validateWithSchema(doc, generateClassSchema() as Record<string, unknown>);
|
|
674
|
+
}
|
|
675
|
+
|
|
635
676
|
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
636
677
|
|
|
637
678
|
async function runSchemaCli() {
|
|
@@ -674,6 +715,30 @@ async function runSchemaCli() {
|
|
|
674
715
|
writeFileSync(resolve(schemaDir, "schemas", "project.fields.schema.json"), fieldsStr, "utf8");
|
|
675
716
|
writeFileSync(resolve(schemaDir, "schemas", "document.paths.schema.json"), pathsStr, "utf8");
|
|
676
717
|
writeFileSync(resolve(schemaDir, "extension-manifest.schema.json"), manifestStr, "utf8");
|
|
718
|
+
// The schemas/ fragments are oxfmt-managed (unlike packages/schema/*.json, which .oxfmtrc
|
|
719
|
+
// Ignores), so emit them formatted — otherwise every `bun run generate:schema` reintroduces
|
|
720
|
+
// Raw-stringify formatting and dirties the tree. This is a dev-hygiene step only: the JSON
|
|
721
|
+
// Above is already written and valid, and oxfmt just prettifies it (e.g. collapsing short enum
|
|
722
|
+
// Arrays onto one line). Inside a sandboxed/offline build (e.g. `nix build`) oxfmt exits
|
|
723
|
+
// Non-zero with no diagnostic output, so treat a failure as non-fatal and keep the valid JSON
|
|
724
|
+
// Rather than aborting the build over cosmetic formatting.
|
|
725
|
+
const fmt = Bun.spawnSync(
|
|
726
|
+
[
|
|
727
|
+
"bunx",
|
|
728
|
+
"oxfmt",
|
|
729
|
+
resolve(schemaDir, "schemas", "project.core.schema.json"),
|
|
730
|
+
resolve(schemaDir, "schemas", "project.fields.schema.json"),
|
|
731
|
+
resolve(schemaDir, "schemas", "document.paths.schema.json"),
|
|
732
|
+
],
|
|
733
|
+
{ cwd: schemaDir },
|
|
734
|
+
);
|
|
735
|
+
if (fmt.exitCode !== 0) {
|
|
736
|
+
const detail =
|
|
737
|
+
fmt.stderr.toString() ||
|
|
738
|
+
fmt.stdout.toString() ||
|
|
739
|
+
`exit ${fmt.exitCode ?? "null"}${fmt.signalCode ? `, signal ${fmt.signalCode}` : ""}`;
|
|
740
|
+
console.error(`Warning: skipped oxfmt reformat of generated schemas (${detail})`);
|
|
741
|
+
}
|
|
677
742
|
console.error("Generated:");
|
|
678
743
|
console.error(" schema.json (component)");
|
|
679
744
|
console.error(" project-schema.json");
|
package/src/validate-project.ts
CHANGED
|
@@ -133,6 +133,19 @@ async function loadProjectSchemaRef(uri: string, root: string): Promise<Record<s
|
|
|
133
133
|
const selfRequire = createRequire(import.meta.url);
|
|
134
134
|
resolved = selfRequire.resolve(specifier);
|
|
135
135
|
} catch (error) {
|
|
136
|
+
// Exports-map subpath resolution is brittle against version-skewed shadowing installs (a
|
|
137
|
+
// Stale local copy terminates Node's walk-up with ERR_PACKAGE_PATH_NOT_EXPORTED). Our own
|
|
138
|
+
// Schema files are pure data and the running validator's copy is guaranteed to match, so
|
|
139
|
+
// Read them straight off this package's disk as the last resort.
|
|
140
|
+
const ownPrefix = "@jxsuite/schema/";
|
|
141
|
+
if (specifier.startsWith(ownPrefix)) {
|
|
142
|
+
const ownFile = fileURLToPath(
|
|
143
|
+
new URL(`../${specifier.slice(ownPrefix.length)}`, import.meta.url),
|
|
144
|
+
);
|
|
145
|
+
if (existsSync(ownFile)) {
|
|
146
|
+
return JSON.parse(readFileSync(ownFile, "utf8")) as Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
136
149
|
throw new Error(
|
|
137
150
|
`Schema ref "${uri}" does not exist and "${specifier}" is not resolvable from the ` +
|
|
138
151
|
`project or the host`,
|