@uniweb/build 0.14.19 → 0.14.21
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 +2 -2
- package/src/resolve-data-schema.js +70 -24
- package/src/site/content-collector.js +13 -2
- 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.21",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"optionalDependencies": {
|
|
66
66
|
"@uniweb/runtime": "0.8.20",
|
|
67
67
|
"@uniweb/content-reader": "1.1.12",
|
|
68
|
-
"@uniweb/schemas": "0.2.
|
|
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
|
|
@@ -295,6 +295,16 @@ const CONTENT_PROFILES = {
|
|
|
295
295
|
'document.yml': { contentDir: 'content', defaultMode: 'pages', orderField: 'content' }
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// Short profile aliases, so a caller that knows its own context can select
|
|
299
|
+
// the profile explicitly (collectSiteContent's `profile` option) instead of
|
|
300
|
+
// inferring it from the config filename. unipress — always a document tool —
|
|
301
|
+
// uses this so an author can name a variant config anything (book.yml,
|
|
302
|
+
// print.yml) without it being misread as a site.
|
|
303
|
+
const PROFILE_ALIASES = {
|
|
304
|
+
document: CONTENT_PROFILES['document.yml'],
|
|
305
|
+
site: CONTENT_PROFILES['site.yml']
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
function getContentProfile(configFile) {
|
|
299
309
|
return CONTENT_PROFILES[configFile] || CONTENT_PROFILES['site.yml']
|
|
300
310
|
}
|
|
@@ -1974,17 +1984,18 @@ async function collectLayouts(layoutDir, siteRoot, layoutNames = new Set()) {
|
|
|
1974
1984
|
* @param {Object} options - Collection options
|
|
1975
1985
|
* @param {string} options.foundationPath - Path to foundation directory (for theme vars)
|
|
1976
1986
|
* @param {string} [options.configFile='site.yml'] - Name of the top-level config file inside sitePath. Defaults to 'site.yml'. Document tools (unipress) pass 'document.yml'.
|
|
1987
|
+
* @param {'document'|'site'} [options.profile] - Explicit content profile, authoritative over the filename. Lets a tool that knows its context (unipress → 'document') read an arbitrarily named config with the right directory/mode/ordering. Falls back to the configFile-derived profile when omitted.
|
|
1977
1988
|
* @returns {Promise<Object>} Site content object with assets manifest
|
|
1978
1989
|
*/
|
|
1979
1990
|
export async function collectSiteContent(sitePath, options = {}) {
|
|
1980
|
-
const { foundationPath, configFile = 'site.yml' } = options
|
|
1991
|
+
const { foundationPath, configFile = 'site.yml', profile: profileName } = options
|
|
1981
1992
|
|
|
1982
1993
|
// Read site config and raw theme config
|
|
1983
1994
|
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
|
1984
1995
|
|
|
1985
1996
|
// Profile selects workspace-root defaults: site.yml → pages/ + page mode +
|
|
1986
1997
|
// pages: ordering; document.yml → content/ + folder mode + content: ordering.
|
|
1987
|
-
const profile = getContentProfile(configFile)
|
|
1998
|
+
const profile = (profileName && PROFILE_ALIASES[profileName]) || getContentProfile(configFile)
|
|
1988
1999
|
|
|
1989
2000
|
// Resolve content paths from <config>.paths: group, defaulting per profile.
|
|
1990
2001
|
// Backward compatibility: when the document profile's `content/` is missing
|
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
|