@uniweb/build 0.14.19 → 0.14.20
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 +3 -3
- package/src/resolve-data-schema.js +70 -24
- package/src/uwx/data-schema.js +14 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.20",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"@uniweb/theming": "0.1.4"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"@uniweb/runtime": "0.8.20",
|
|
67
66
|
"@uniweb/content-reader": "1.1.12",
|
|
68
|
-
"@uniweb/
|
|
67
|
+
"@uniweb/runtime": "0.8.20",
|
|
68
|
+
"@uniweb/schemas": "0.2.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -57,21 +57,21 @@ const TYPE_ALIASES = {
|
|
|
57
57
|
}
|
|
58
58
|
// Friendly type aliases that lower to a base kind + a carried `format` marker.
|
|
59
59
|
// `url`/`email` → `string` (server-validated value subtypes). `markdown`/`html` →
|
|
60
|
-
// `text` (
|
|
61
|
-
// what the retired `richtext` kind used to be: 2026-06-02 / uwx-format.md).
|
|
60
|
+
// `text` (a file-based rich-content body — round-trips as the raw source string).
|
|
62
61
|
const FORMAT_TYPE_ALIASES = {
|
|
63
62
|
url: { type: 'string', format: 'url' },
|
|
64
63
|
email: { type: 'string', format: 'email' },
|
|
65
64
|
markdown: { type: 'text', format: 'markdown' },
|
|
66
65
|
html: { type: 'text', format: 'html' },
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
66
|
+
// `richtext` → a ProseMirror rich document (`json` + `format: prosemirror`): the
|
|
67
|
+
// framework's standard way to represent rich text — the structured, lossless form
|
|
68
|
+
// the visual app edits (text, media, tables, code, data blocks, icons, and inline
|
|
69
|
+
// components). Synced to file mode as enhanced markdown via content-writer. Contrast
|
|
70
|
+
// `markdown`/`html`, which are source-string bodies (raw text, no structured editor).
|
|
71
|
+
richtext: { type: 'json', format: 'prosemirror' },
|
|
71
72
|
}
|
|
72
73
|
// The advertised format-aliasing type words (drives the "Known types" hint).
|
|
73
|
-
|
|
74
|
-
export const FORMAT_TYPES = new Set(['url', 'email', 'markdown', 'html'])
|
|
74
|
+
export const FORMAT_TYPES = new Set(['url', 'email', 'markdown', 'html', 'richtext'])
|
|
75
75
|
export const SECTION_KINDS = new Set(['single', 'multi', 'binder'])
|
|
76
76
|
|
|
77
77
|
// Scope → schema package resolution. The shared standard schemas are referenced
|
|
@@ -324,7 +324,7 @@ export function validateAndNormalizeSchema(schema, ref) {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
const out = {}
|
|
327
|
-
for (const k of ['name', 'version', 'description', 'sortDate']) {
|
|
327
|
+
for (const k of ['name', 'version', 'description', 'sort_date', 'sortDate']) {
|
|
328
328
|
if (schema[k] !== undefined) out[k] = schema[k]
|
|
329
329
|
}
|
|
330
330
|
|
|
@@ -361,7 +361,18 @@ function normalizeSection(section, ref, path, briefState) {
|
|
|
361
361
|
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
362
362
|
throw new Error(`Data schema '${ref}': section '${path}' must be an object.`)
|
|
363
363
|
}
|
|
364
|
-
|
|
364
|
+
if (section.many !== undefined && typeof section.many !== 'boolean') {
|
|
365
|
+
throw new Error(`Data schema '${ref}': section '${path}' 'many' must be a boolean.`)
|
|
366
|
+
}
|
|
367
|
+
// Cardinality. Friendly sugar: `many: true` → a list of records; a section with
|
|
368
|
+
// only child `sections:` (no `fields:`) is a binder — inferred, never written.
|
|
369
|
+
// Explicit `kind:` is still honored (the lower-level form it normalizes to).
|
|
370
|
+
let kind = section.kind
|
|
371
|
+
if (kind === undefined) {
|
|
372
|
+
if (section.many === true) kind = 'multi'
|
|
373
|
+
else if (section.fields === undefined && section.sections !== undefined) kind = 'binder'
|
|
374
|
+
else kind = 'single'
|
|
375
|
+
}
|
|
365
376
|
if (!SECTION_KINDS.has(kind)) {
|
|
366
377
|
throw new Error(`Data schema '${ref}': section '${path}' has invalid kind '${kind}' (expected single | multi | binder).`)
|
|
367
378
|
}
|
|
@@ -369,7 +380,7 @@ function normalizeSection(section, ref, path, briefState) {
|
|
|
369
380
|
|
|
370
381
|
if (section.brief === true) {
|
|
371
382
|
if (kind !== 'single') {
|
|
372
|
-
throw new Error(`Data schema '${ref}': brief section '${path}' must be
|
|
383
|
+
throw new Error(`Data schema '${ref}': brief section '${path}' must be a single record (drop 'many').`)
|
|
373
384
|
}
|
|
374
385
|
if (++briefState.count > 1) {
|
|
375
386
|
throw new Error(`Data schema '${ref}': more than one section marked 'brief: true' (at most one).`)
|
|
@@ -395,18 +406,19 @@ function normalizeSection(section, ref, path, briefState) {
|
|
|
395
406
|
}
|
|
396
407
|
if (section.constraints !== undefined) out.constraints = section.constraints
|
|
397
408
|
|
|
398
|
-
// `
|
|
399
|
-
// Carried into the IR so the
|
|
400
|
-
// `self_nesting`. The
|
|
401
|
-
// (`parent_item_id`); no explicit field expresses it.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
// `tree: true` (friendly) / `nestable: true` (lower-level) — a list section whose
|
|
410
|
+
// records form a tree among themselves. Carried into the IR so the lowering maps
|
|
411
|
+
// it to the model's `self_nesting`. The parent/child link is internal to the
|
|
412
|
+
// backend (`parent_item_id`); no explicit field expresses it.
|
|
413
|
+
const treeFlag = section.tree ?? section.nestable
|
|
414
|
+
if (treeFlag !== undefined) {
|
|
415
|
+
if (typeof treeFlag !== 'boolean') {
|
|
416
|
+
throw new Error(`Data schema '${ref}': section '${path}' 'tree' must be a boolean.`)
|
|
405
417
|
}
|
|
406
|
-
if (
|
|
407
|
-
throw new Error(`Data schema '${ref}': section '${path}' is '
|
|
418
|
+
if (treeFlag && kind !== 'multi') {
|
|
419
|
+
throw new Error(`Data schema '${ref}': section '${path}' is 'tree: true' but not a list — only a 'many: true' section can form a tree.`)
|
|
408
420
|
}
|
|
409
|
-
if (
|
|
421
|
+
if (treeFlag) out.nestable = true
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
// `append_only` — a multi whose records are insert-only: the backend accepts
|
|
@@ -419,7 +431,7 @@ function normalizeSection(section, ref, path, briefState) {
|
|
|
419
431
|
throw new Error(`Data schema '${ref}': section '${path}' 'append_only' must be a boolean.`)
|
|
420
432
|
}
|
|
421
433
|
if (section.append_only && kind !== 'multi') {
|
|
422
|
-
throw new Error(`Data schema '${ref}': section '${path}' is 'append_only: true' but
|
|
434
|
+
throw new Error(`Data schema '${ref}': section '${path}' is 'append_only: true' but not a list — only a 'many: true' section can be append-only.`)
|
|
423
435
|
}
|
|
424
436
|
if (section.append_only) out.append_only = true
|
|
425
437
|
}
|
|
@@ -444,6 +456,40 @@ function normalizeField(field, ref, path) {
|
|
|
444
456
|
if (!field || typeof field !== 'object' || Array.isArray(field)) {
|
|
445
457
|
throw new Error(`Data schema '${ref}': field '${path}' must be an object or a type string.`)
|
|
446
458
|
}
|
|
459
|
+
|
|
460
|
+
// Sugar: `many: true` → a list. Wrap the field-minus-`many` as the array's item
|
|
461
|
+
// type (lowers to the canonical `multiple`). The common cases —
|
|
462
|
+
// `{ ref: '@/x', many: true }`, `{ type: string, many: true }` — read as "a list
|
|
463
|
+
// of X" with no `array`/`items` ceremony.
|
|
464
|
+
if (field.many !== undefined) {
|
|
465
|
+
if (typeof field.many !== 'boolean') {
|
|
466
|
+
throw new Error(`Data schema '${ref}': field '${path}' 'many' must be a boolean.`)
|
|
467
|
+
}
|
|
468
|
+
if (field.many) {
|
|
469
|
+
// Collection-level metadata (required, default, label, help, description)
|
|
470
|
+
// rides on the array; the type-bearing attributes describe each item.
|
|
471
|
+
const ITEM_KEYS = new Set(['type', 'ref', 'options', 'enum', 'fields', 'items', 'format'])
|
|
472
|
+
const out = { type: 'array' }
|
|
473
|
+
const item = {}
|
|
474
|
+
for (const [k, v] of Object.entries(field)) {
|
|
475
|
+
if (k === 'many') continue
|
|
476
|
+
if (ITEM_KEYS.has(k)) item[k] = v
|
|
477
|
+
else out[k] = v
|
|
478
|
+
}
|
|
479
|
+
out.items = normalizeField(item, ref, `${path}[]`)
|
|
480
|
+
return out
|
|
481
|
+
}
|
|
482
|
+
const { many, ...rest } = field // many: false → a single value
|
|
483
|
+
field = rest
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Sugar: infer `type` from `ref:`/`options:` when omitted — `{ ref: '@/x' }` is a
|
|
487
|
+
// reference; `{ options: '@/x' }` is a curated picklist value.
|
|
488
|
+
if (field.type === undefined) {
|
|
489
|
+
if (typeof field.ref === 'string') field = { ...field, type: 'ref' }
|
|
490
|
+
else if (typeof field.options === 'string') field = { ...field, type: 'string' }
|
|
491
|
+
}
|
|
492
|
+
|
|
447
493
|
const rawType = field.type
|
|
448
494
|
if (typeof rawType !== 'string') {
|
|
449
495
|
throw new Error(`Data schema '${ref}': field '${path}' has no 'type'.`)
|
|
@@ -455,8 +501,8 @@ function normalizeField(field, ref, path) {
|
|
|
455
501
|
if (field[k] !== undefined) out[k] = field[k]
|
|
456
502
|
}
|
|
457
503
|
|
|
458
|
-
// Resolve the type: format-aliases (url/email → string; markdown/html
|
|
459
|
-
//
|
|
504
|
+
// Resolve the type: format-aliases (url/email → string; markdown/html → text;
|
|
505
|
+
// richtext → json) carry a `format` marker; else the plain alias map; else verbatim.
|
|
460
506
|
const formatAlias = FORMAT_TYPE_ALIASES[rawType]
|
|
461
507
|
if (formatAlias) {
|
|
462
508
|
out.type = formatAlias.type
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -194,16 +194,21 @@ function lowerField(rawField, resolve, optResolve) {
|
|
|
194
194
|
return out
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// A leaf (scalar) kind. `richtext` is
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
197
|
+
// A leaf (scalar) kind. `richtext` is NOT a kind — it is the author alias for a
|
|
198
|
+
// ProseMirror document (`json` + `format: prosemirror`), normalized upstream in
|
|
199
|
+
// resolve-data-schema.js, so normalized IR never carries a raw `richtext` kind. The
|
|
200
|
+
// only way one could reach here is a STALE prebuilt schema.json (a foundation built
|
|
201
|
+
// before the 2026-06-02 kind retirement and loaded from dist/meta/schema.json without
|
|
202
|
+
// re-resolving). Fail locally — rebuild the foundation — rather than ship a kind the
|
|
203
|
+
// backend rejects.
|
|
204
|
+
if (type === 'richtext') {
|
|
205
|
+
throw new Error(
|
|
206
|
+
'This foundation carries the retired `richtext` kind in its built schema — ' +
|
|
207
|
+
'rebuild it (`richtext` is now json + format: prosemirror).'
|
|
208
|
+
)
|
|
206
209
|
}
|
|
210
|
+
const leafType = type
|
|
211
|
+
const leafFormat = field.format
|
|
207
212
|
|
|
208
213
|
const out = { type: leafType }
|
|
209
214
|
if (field.label) out.label = field.label
|