@uniweb/build 0.75.1 → 0.76.1
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 +7 -7
- package/src/dev/plugin.js +3 -3
- package/src/generate-entry.js +15 -28
- package/src/prerender.js +4 -1
- package/src/resolve-data-schema.js +66 -13
- package/src/runtime-schema.js +54 -218
- package/src/uwx/data-schema.js +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/
|
|
60
|
+
"@uniweb/content-reader": "^1.2.8",
|
|
61
61
|
"@uniweb/content-writer": "^0.3.4",
|
|
62
|
+
"@uniweb/projections": "^0.7.14",
|
|
63
|
+
"@uniweb/schemas": "^0.12.0",
|
|
62
64
|
"@uniweb/semantic-parser": "^1.5.1",
|
|
63
|
-
"@uniweb/content-reader": "^1.2.8",
|
|
64
|
-
"@uniweb/schemas": "^0.10.2",
|
|
65
65
|
"@uniweb/theming": "^0.2.0"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.
|
|
68
|
+
"@uniweb/runtime": "^0.29.1"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/runtime": "^0.
|
|
78
|
-
"@uniweb/core": "^0.
|
|
77
|
+
"@uniweb/runtime": "^0.29.1",
|
|
78
|
+
"@uniweb/core": "^0.37.1"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/dev/plugin.js
CHANGED
|
@@ -243,9 +243,9 @@ export function foundationDevPlugin(options = {}) {
|
|
|
243
243
|
// Ignore generated files (build output triggers entry regeneration)
|
|
244
244
|
if (!filename || filename.includes('_entry.generated')) return
|
|
245
245
|
|
|
246
|
-
// Rebuild for source file changes, and for
|
|
247
|
-
//
|
|
248
|
-
//
|
|
246
|
+
// Rebuild for source file changes, and for the other structural files
|
|
247
|
+
// (`shouldRegenerateForFile`) — a data schema under `schemas/`, `.yml` or `.json`
|
|
248
|
+
// as often as `.js`, moves the rebuilt schema.json
|
|
249
249
|
if (
|
|
250
250
|
filename.endsWith('.js') ||
|
|
251
251
|
filename.endsWith('.jsx') ||
|
package/src/generate-entry.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The `meta` export contains only properties needed at runtime:
|
|
13
13
|
* - `background` - 'self' opt-out when component handles its own background
|
|
14
14
|
* - `data` - the `content.data` keys the component declares, with their schema refs
|
|
15
|
-
* - `
|
|
15
|
+
* - `vars` - the CSS variables the component declares, with their defaults
|
|
16
16
|
* - `defaults` - Param default values
|
|
17
17
|
* - `context` - Static capabilities for cross-block coordination
|
|
18
18
|
* - `initialState` - Initial values for mutable block state
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
LAYOUTS_PATH
|
|
33
33
|
} from './schema.js'
|
|
34
34
|
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas, extractFoundationRuntime } from './runtime-schema.js'
|
|
35
|
-
import {
|
|
35
|
+
import { SCHEMA_EXTENSIONS } from './resolve-data-schema.js'
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* The files a foundation's data schemas are read from, relative to its source root:
|
|
@@ -196,12 +196,12 @@ function generateEntrySource(components, options = {}) {
|
|
|
196
196
|
lines.push('const capabilities = null')
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
// Per-component runtime metadata (defaults, context, initialState, background, data)
|
|
199
|
+
// Per-component runtime metadata (defaults, context, initialState, background, data, vars)
|
|
200
200
|
lines.push('')
|
|
201
201
|
const metaJson = JSON.stringify(Object.keys(meta).length > 0 ? meta : {}, null, 2)
|
|
202
202
|
lines.push(`const meta = ${metaJson}`)
|
|
203
203
|
|
|
204
|
-
// Per-layout runtime metadata (
|
|
204
|
+
// Per-layout runtime metadata (transitions, layers, scroll, defaults)
|
|
205
205
|
lines.push('')
|
|
206
206
|
const layoutMetaJson = JSON.stringify(Object.keys(layoutMeta).length > 0 ? layoutMeta : {}, null, 2)
|
|
207
207
|
lines.push(`const layoutMeta = ${layoutMetaJson}`)
|
|
@@ -311,23 +311,12 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
311
311
|
const foundationConfig = await loadFoundationConfig(srcDir)
|
|
312
312
|
const foundationRuntime = foundationExports ? extractFoundationRuntime(foundationConfig) : { code: [], data: {} }
|
|
313
313
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
// foundation-tier key reaches every section (`declaredKeys`) but has nowhere to
|
|
321
|
-
// put its lean schema in a PER-COMPONENT runtime meta, so resolving its ref here
|
|
322
|
-
// would produce an entry nothing reads.
|
|
323
|
-
//
|
|
324
|
-
// ⚠️ The consequence is real and still open: a foundation-tier `data:` entry
|
|
325
|
-
// naming a schema gets no field defaults from `applySchemas` at runtime. Latent
|
|
326
|
-
// today — every foundation-tier declaration in the templates is ref-less — and
|
|
327
|
-
// fixing it means deciding where a foundation-wide schema lives in the runtime
|
|
328
|
-
// meta, which is a design question, not a missing argument.
|
|
329
|
-
const dataSchemaMap = await buildDataSchemaMap(collectSchemaRefs(components), { srcDir })
|
|
330
|
-
const meta = extractAllRuntimeSchemas(components, dataSchemaMap)
|
|
314
|
+
// Per-component runtime metadata. ⭐ No data schema is resolved for it: the runtime reads a
|
|
315
|
+
// declared key's ref and nothing of its shape (2026-10-05), so the entry no longer depends on
|
|
316
|
+
// a schema file — `schema.js` resolves them, for schema.json, and a missing one stops the
|
|
317
|
+
// build there. ⛔ Until then the entry carried each key's field defaults, resolved here, and a
|
|
318
|
+
// foundation-tier `data:` key got none, having no per-component place to put them.
|
|
319
|
+
const meta = extractAllRuntimeSchemas(components)
|
|
331
320
|
|
|
332
321
|
// Extract per-layout runtime metadata from meta.js files
|
|
333
322
|
const layoutMeta = extractAllLayoutRuntimeSchemas(layouts)
|
|
@@ -519,13 +508,11 @@ export function shouldRegenerateForFile(file, srcDir) {
|
|
|
519
508
|
}
|
|
520
509
|
}
|
|
521
510
|
|
|
522
|
-
//
|
|
523
|
-
// The entry
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
// `schemas/member.yml` exists fails the regeneration, and until 2026-09-14 writing
|
|
528
|
-
// the file triggered no other — the dev server kept the entry without the key.
|
|
511
|
+
// The foundation's data schemas — `schemas/<name>.<ext>` and `schemas.config.js`.
|
|
512
|
+
// ⚠️ The entry holds nothing read from one since 2026-10-05 (no field defaults), so
|
|
513
|
+
// regenerating it changes nothing. The match stays for the dev rebuild of a
|
|
514
|
+
// runtime-linked foundation (`dev/plugin.js`), which also writes the editor's
|
|
515
|
+
// schema.json — whose `dataSchemas` a schema change does move.
|
|
529
516
|
if (rel.startsWith(`${SCHEMAS_PATH}/`)) {
|
|
530
517
|
const inner = rel.slice(SCHEMAS_PATH.length + 1)
|
|
531
518
|
if (!inner.includes('/') && SCHEMA_EXTENSIONS.some(ext => inner.endsWith(ext))) {
|
package/src/prerender.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
deriveCacheKey,
|
|
24
24
|
evaluateQuery,
|
|
25
25
|
} from '@uniweb/core'
|
|
26
|
-
import { recordTitle, routePatternToRegex, routeFieldOf } from '@uniweb/core/route-match'
|
|
26
|
+
import { recordTitle, recordImage, routePatternToRegex, routeFieldOf } from '@uniweb/core/route-match'
|
|
27
27
|
import DataStore from '@uniweb/core/datastore'
|
|
28
28
|
import { executeFetch, mergeDataIntoContent, toFetchList } from './site/data-fetcher.js'
|
|
29
29
|
import { createFileTransport } from './site/file-transport.js'
|
|
@@ -473,6 +473,9 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
473
473
|
const title = recordTitle(item)
|
|
474
474
|
if (title) concretePage.title = title
|
|
475
475
|
if (item.description || item.excerpt) concretePage.description = item.description || item.excerpt
|
|
476
|
+
// ⭐ And its picture is what a link to the page shows (`recordImage`) — the SPA's rule.
|
|
477
|
+
const image = recordImage(item)
|
|
478
|
+
if (image) concretePage.seo = { ...(concretePage.seo || {}), image }
|
|
476
479
|
|
|
477
480
|
expandedPages.push(concretePage)
|
|
478
481
|
}
|
|
@@ -84,6 +84,60 @@ export const SCHEMA_NOT_FOUND = 'SCHEMA_NOT_FOUND'
|
|
|
84
84
|
const RESERVED_SYSTEM_SCOPE = 'uniweb'
|
|
85
85
|
const packageForScope = (scope) => SCOPE_PACKAGE[scope] ?? `@${scope}/schemas`
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* A NAMED data schema — a `schemas/` file, `@std/<name>`, `@org/<name>` — validated and
|
|
89
|
+
* normalized, and refused when a field declares a `default`.
|
|
90
|
+
*
|
|
91
|
+
* ⛔ A named schema declares no default — ruled 2026-10-05 [Diego], when the runtime stopped
|
|
92
|
+
* filling fields: nothing would read one. The runtime fills no field, the declaration a
|
|
93
|
+
* registry stores carries none (`uwx/data-schema.js`), and the foundation schema an editor
|
|
94
|
+
* reads leaves `dataSchemas` out (`uwx/registry-package.js`) — so a `default:` here would be a
|
|
95
|
+
* promise kept nowhere. What an absent field renders as is the component's choice. A default
|
|
96
|
+
* stays where an editor reads it: a component's own inline field map or form in `data:`.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} schema - the schema as authored
|
|
99
|
+
* @param {string} ref - its ref, for the message
|
|
100
|
+
* @returns {object} the normalized schema
|
|
101
|
+
*/
|
|
102
|
+
function normalizeNamedSchema(schema, ref) {
|
|
103
|
+
const normalized = validateAndNormalizeSchema(schema, ref)
|
|
104
|
+
const paths = fieldsWithDefaults(normalized)
|
|
105
|
+
if (paths.length > 0) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Data schema '${ref}': a named data schema declares no default — remove \`default:\` from ` +
|
|
108
|
+
`${paths.map((p) => `'${p}'`).join(', ')}. A component decides what an absent field ` +
|
|
109
|
+
`renders as; an editor pre-fills from a component's own inline field map or form in \`data:\`.`
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return normalized
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The dotted paths of every field in a normalized schema that declares a `default`. */
|
|
116
|
+
function fieldsWithDefaults(schema) {
|
|
117
|
+
const found = []
|
|
118
|
+
const walkFields = (fields, prefix) => {
|
|
119
|
+
if (!fields || typeof fields !== 'object') return
|
|
120
|
+
for (const [name, spec] of Object.entries(fields)) walkSpec(spec, `${prefix}${name}`)
|
|
121
|
+
}
|
|
122
|
+
const walkSpec = (spec, path) => {
|
|
123
|
+
if (!spec || typeof spec !== 'object') return
|
|
124
|
+
if (spec.default !== undefined) found.push(path)
|
|
125
|
+
walkFields(spec.fields, `${path}.`)
|
|
126
|
+
if (spec.items) walkSpec(spec.items, `${path}[]`)
|
|
127
|
+
if (spec.values) walkSpec(spec.values, `${path}{}`)
|
|
128
|
+
}
|
|
129
|
+
const walkSections = (sections, prefix) => {
|
|
130
|
+
if (!sections || typeof sections !== 'object') return
|
|
131
|
+
for (const [name, section] of Object.entries(sections)) {
|
|
132
|
+
walkFields(section?.fields, `${prefix}${name}.`)
|
|
133
|
+
walkSections(section?.sections, `${prefix}${name}.`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
walkFields(schema?.fields, '')
|
|
137
|
+
walkSections(schema?.sections, '')
|
|
138
|
+
return found
|
|
139
|
+
}
|
|
140
|
+
|
|
87
141
|
/**
|
|
88
142
|
* Collect every distinct schema ref used by a foundation's section bindings.
|
|
89
143
|
* Reads `data: { key: '<ref>' }` (short) and `data: { key: { schema: '<ref>' } }`
|
|
@@ -94,9 +148,9 @@ const packageForScope = (scope) => SCOPE_PACKAGE[scope] ?? `@${scope}/schemas`
|
|
|
94
148
|
* (`declaredKeys`, `@uniweb/core/data-keys`) — but only components were scanned
|
|
95
149
|
* here, so a ref at that tier was neither resolved into `dataSchemas` nor
|
|
96
150
|
* rejected when dangling, while the key was still delivered at runtime. The
|
|
97
|
-
* symptom would have been no schema for the editor
|
|
98
|
-
*
|
|
99
|
-
*
|
|
151
|
+
* symptom would have been no schema for the editor, with no build error. It bit
|
|
152
|
+
* nobody only because every foundation-tier declaration in the templates is
|
|
153
|
+
* ref-less.
|
|
100
154
|
*
|
|
101
155
|
* @param {Object} components - Map of componentName → full meta (with `data`)
|
|
102
156
|
* @param {Object|false|null} [foundationData] - the foundation's `main.js` `data:`
|
|
@@ -136,7 +190,7 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
136
190
|
{ code: SCHEMA_NOT_FOUND }
|
|
137
191
|
)
|
|
138
192
|
}
|
|
139
|
-
return
|
|
193
|
+
return normalizeNamedSchema(await loadSchemaFile(file), ref)
|
|
140
194
|
}
|
|
141
195
|
|
|
142
196
|
// ⭐ THE SYSTEM MODELS A SITE'S RECORDS MAY NAME — `@uniweb/link` — resolve from the build's own
|
|
@@ -145,7 +199,7 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
145
199
|
// never a data-schema source. (The whole scope was refused until 2026-09-28.)
|
|
146
200
|
if (scope === RESERVED_SYSTEM_SCOPE) {
|
|
147
201
|
const system = SYSTEM_RECORD_SCHEMAS[name]
|
|
148
|
-
if (system) return
|
|
202
|
+
if (system) return normalizeNamedSchema(system, ref)
|
|
149
203
|
const allowed = Object.keys(SYSTEM_RECORD_SCHEMAS).map((n) => `'@${scope}/${n}'`).join(', ')
|
|
150
204
|
throw new Error(
|
|
151
205
|
`'@${scope}' is the reserved platform system namespace: the only data schemas a site's records ` +
|
|
@@ -170,7 +224,7 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
170
224
|
`exists there. Point it at a ${SCHEMA_EXTENSIONS.join(' / ')} file.`
|
|
171
225
|
)
|
|
172
226
|
}
|
|
173
|
-
return
|
|
227
|
+
return normalizeNamedSchema(await loadSchemaFile(file), ref)
|
|
174
228
|
}
|
|
175
229
|
|
|
176
230
|
// A scope alias ('@org' → a DIR) resolves 'name' to a bare schema FILE in that
|
|
@@ -195,7 +249,7 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
195
249
|
}
|
|
196
250
|
throw new Error(msg)
|
|
197
251
|
}
|
|
198
|
-
return
|
|
252
|
+
return normalizeNamedSchema(await loadSchemaFile(file), ref)
|
|
199
253
|
}
|
|
200
254
|
|
|
201
255
|
// Every other scope is an org namespace: '@org/name' resolves `name` from that
|
|
@@ -207,7 +261,7 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
207
261
|
if (!schema) {
|
|
208
262
|
throw new Error(`Unknown data schema '${ref}': '${pkg}' exports no schema named '${name}'.`)
|
|
209
263
|
}
|
|
210
|
-
return
|
|
264
|
+
return normalizeNamedSchema(schema, ref)
|
|
211
265
|
}
|
|
212
266
|
|
|
213
267
|
/**
|
|
@@ -224,9 +278,8 @@ export async function resolveSchemaRef(ref, { srcDir, aliases }) {
|
|
|
224
278
|
export async function buildDataSchemaMap(refs, { srcDir }) {
|
|
225
279
|
// Load the foundation's optional schemas.config.js once; every ref (and every
|
|
226
280
|
// transitively-discovered ref) resolves against the same alias map. This is
|
|
227
|
-
// the single entry
|
|
228
|
-
//
|
|
229
|
-
// resolve refs identically.
|
|
281
|
+
// the single entry schema discovery and `uniweb validate` go through, so the
|
|
282
|
+
// editor's schema.json and the check of a site's data resolve refs identically.
|
|
230
283
|
const aliases = await loadSchemaAliases(srcDir)
|
|
231
284
|
const map = {}
|
|
232
285
|
const queue = Array.from(refs)
|
|
@@ -299,7 +352,7 @@ async function collectSchemasFromExports(packageDir) {
|
|
|
299
352
|
for (const name of names) {
|
|
300
353
|
const schema = get(name)
|
|
301
354
|
if (!schema || typeof schema !== 'object') continue
|
|
302
|
-
out[`@/${name}`] =
|
|
355
|
+
out[`@/${name}`] = normalizeNamedSchema(schema, `@/${name}`)
|
|
303
356
|
}
|
|
304
357
|
return out
|
|
305
358
|
}
|
|
@@ -309,7 +362,7 @@ async function collectSchemasFromExports(packageDir) {
|
|
|
309
362
|
async function collectSchemasFromDir(dir) {
|
|
310
363
|
const out = {}
|
|
311
364
|
for (const { file, name } of schemaFilesIn(dir)) {
|
|
312
|
-
out[`@/${name}`] =
|
|
365
|
+
out[`@/${name}`] = normalizeNamedSchema(await loadSchemaFile(join(dir, file)), `@/${name}`)
|
|
313
366
|
}
|
|
314
367
|
return out
|
|
315
368
|
}
|
package/src/runtime-schema.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime Schema Extractor
|
|
3
3
|
*
|
|
4
|
-
* Extracts lean runtime-relevant metadata from full meta.js files
|
|
5
|
-
*
|
|
6
|
-
* needed at render time:
|
|
4
|
+
* Extracts lean runtime-relevant metadata from full meta.js files — what a published page
|
|
5
|
+
* reads to render, and nothing else:
|
|
7
6
|
*
|
|
8
7
|
* - background: 'self' when component handles its own background
|
|
9
8
|
* - data: { <key>: <schema ref> | null } — every `content.data` key the component
|
|
10
9
|
* declares, in order, with its schema ref (null for an inline shape)
|
|
11
|
-
* -
|
|
10
|
+
* - vars: { <name>: { default? } } — the CSS variables the component declares
|
|
12
11
|
* - defaults: param default values
|
|
13
12
|
* - context: static capabilities for cross-block coordination
|
|
14
13
|
* - initialState: initial values for mutable block state
|
|
@@ -20,14 +19,20 @@
|
|
|
20
19
|
* delivery was default-on — every key that reached a section — and `data:` was a hint for
|
|
21
20
|
* defaults and the editor; `data: false` was the opt-out, emitted as `inheritData: false`.
|
|
22
21
|
*
|
|
22
|
+
* ⛔ NO FIELD DEFAULTS — ruled 2026-10-05 [Diego]. Each declared key also carried `schemas`
|
|
23
|
+
* until then — its schema's field defaults, with `enum` and the nesting that led to them — and
|
|
24
|
+
* the runtime filled a missing field from its `default` and replaced a value its `enum`
|
|
25
|
+
* rejected. A missing field is the record's own fact, and what it renders as is the
|
|
26
|
+
* component's choice; a copy frozen into the bundle drifts from the schema it was taken from;
|
|
27
|
+
* and checking a value is `uniweb validate`'s and the push's job, not the page's. A form in
|
|
28
|
+
* `data:` is the editor's, for authoring the block: none of it reaches the runtime.
|
|
29
|
+
* ⛔ Nor `inset` (an editor's flag — an inset is found by name) or a layout's `areas` (a page's
|
|
30
|
+
* areas are its site's `layout/` folder): no runtime reader read either, until 2026-10-05.
|
|
31
|
+
*
|
|
23
32
|
* Full metadata (titles, descriptions, hints, etc.) stays in schema.json
|
|
24
33
|
* for the visual editor.
|
|
25
34
|
*/
|
|
26
35
|
|
|
27
|
-
import { isRichSchema } from '@uniweb/core'
|
|
28
|
-
import { dataRefOf } from '@uniweb/core/data-keys'
|
|
29
|
-
import { briefFieldMap, wholeFieldMap } from '@uniweb/schemas/conform'
|
|
30
|
-
import { enumValues } from '@uniweb/schemas/format'
|
|
31
36
|
import { lowerData } from '@uniweb/schemas/content'
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -42,188 +47,6 @@ function dataRef(value) {
|
|
|
42
47
|
return value && typeof value === 'object' && typeof value.schema === 'string' && value.schema ? value.schema : null
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
/**
|
|
46
|
-
* Extract lean schema field for runtime
|
|
47
|
-
* Strips editor-only fields (label, hint, description)
|
|
48
|
-
* Keeps runtime fields (type, default, enum, options, fields, items)
|
|
49
|
-
*
|
|
50
|
-
* Vocabulary is the data-schema format:
|
|
51
|
-
* an `object` field nests via `fields:` (a field map); an `array` field nests
|
|
52
|
-
* via `items:` (a single element field). This matches what
|
|
53
|
-
* resolve-data-schema.js normalizes named refs to, so named-ref and inline
|
|
54
|
-
* `data:` schemas share one shape.
|
|
55
|
-
*
|
|
56
|
-
* @param {string|Object} field - Schema field definition
|
|
57
|
-
* @returns {string|Object} - Lean field definition
|
|
58
|
-
*/
|
|
59
|
-
function extractSchemaField(field) {
|
|
60
|
-
// Shorthand: a bare type string ('string', 'decimal', …).
|
|
61
|
-
if (typeof field === 'string') {
|
|
62
|
-
return field
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!field || typeof field !== 'object') {
|
|
66
|
-
return field
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const lean = {}
|
|
70
|
-
|
|
71
|
-
// Keep runtime-relevant fields: the default, plus the inline picklist
|
|
72
|
-
// (`enum`) used for value validation. `options` is a curated-ref string —
|
|
73
|
-
// inert at runtime but carried through.
|
|
74
|
-
if (field.type) lean.type = field.type
|
|
75
|
-
if (field.default !== undefined) lean.default = field.default
|
|
76
|
-
// The values only: an entry's label (`{ value, label }`) is an editor's, and the runtime compares.
|
|
77
|
-
if (field.enum !== undefined) lean.enum = enumValues(field.enum)
|
|
78
|
-
if (field.options) lean.options = field.options
|
|
79
|
-
|
|
80
|
-
// Nested object → recurse into its field map.
|
|
81
|
-
if (field.type === 'object' && field.fields && typeof field.fields === 'object') {
|
|
82
|
-
lean.fields = extractSchemaFields(field.fields)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Array → recurse into its single element field (which may itself be an
|
|
86
|
-
// object carrying nested `fields`).
|
|
87
|
-
if (field.type === 'array' && field.items !== undefined) {
|
|
88
|
-
lean.items = extractSchemaField(field.items)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// If the only thing left is `type`, collapse to the bare type string.
|
|
92
|
-
const keys = Object.keys(lean)
|
|
93
|
-
if (keys.length === 1 && keys[0] === 'type') {
|
|
94
|
-
return lean.type
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return keys.length > 0 ? lean : null
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Extract lean schema fields for an entire schema object
|
|
102
|
-
*
|
|
103
|
-
* @param {Object} schemaFields - Map of fieldName -> field definition
|
|
104
|
-
* @returns {Object} - Map of fieldName -> lean field definition
|
|
105
|
-
*/
|
|
106
|
-
function extractSchemaFields(schemaFields) {
|
|
107
|
-
if (!schemaFields || typeof schemaFields !== 'object') {
|
|
108
|
-
return {}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const lean = {}
|
|
112
|
-
for (const [name, field] of Object.entries(schemaFields)) {
|
|
113
|
-
const leanField = extractSchemaField(field)
|
|
114
|
-
if (leanField !== null) {
|
|
115
|
-
lean[name] = leanField
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return lean
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check if a schema value is in the full @uniweb/schemas format
|
|
123
|
-
* Full format has: { name, version?, description?, fields: { fieldName: fieldDef, ... } }
|
|
124
|
-
*
|
|
125
|
-
* The distinguishing feature is that `fields` is a *keyed object*, not an array.
|
|
126
|
-
* (A rich form schema also has `fields`, but as an array.)
|
|
127
|
-
*
|
|
128
|
-
* @param {Object} schema - Schema value to check
|
|
129
|
-
* @returns {boolean}
|
|
130
|
-
*/
|
|
131
|
-
function isFullSchemaFormat(schema) {
|
|
132
|
-
return (
|
|
133
|
-
schema &&
|
|
134
|
-
typeof schema === 'object' &&
|
|
135
|
-
typeof schema.fields === 'object' &&
|
|
136
|
-
schema.fields !== null &&
|
|
137
|
-
!Array.isArray(schema.fields)
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Pass a rich form schema through with minimal normalization.
|
|
143
|
-
*
|
|
144
|
-
* Rich schemas are passed to the editor (for FormBlock UI rendering) and to
|
|
145
|
-
* the runtime (for default application). We keep all authored metadata so the
|
|
146
|
-
* editor has what it needs; we do not strip editor-only fields here because
|
|
147
|
-
* the same schema feeds both audiences.
|
|
148
|
-
*
|
|
149
|
-
* Normalizations:
|
|
150
|
-
*
|
|
151
|
-
* @param {Object} schema - Rich schema as authored
|
|
152
|
-
* @returns {Object} - Normalized rich schema
|
|
153
|
-
*/
|
|
154
|
-
function normalizeRichSchema(schema) {
|
|
155
|
-
return normalizeRichSchemaValue(schema)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function normalizeRichSchemaValue(value) {
|
|
159
|
-
if (Array.isArray(value)) {
|
|
160
|
-
return value.map(normalizeRichSchemaValue)
|
|
161
|
-
}
|
|
162
|
-
if (!value || typeof value !== 'object') return value
|
|
163
|
-
const out = {}
|
|
164
|
-
for (const [key, v] of Object.entries(value)) {
|
|
165
|
-
if (v && typeof v === 'object') {
|
|
166
|
-
out[key] = normalizeRichSchemaValue(v)
|
|
167
|
-
} else {
|
|
168
|
-
out[key] = v
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return out
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Lean a single `data:` entry value into the runtime field structure that
|
|
176
|
-
* prepare-props.applySchemas applies. A `data:` value is one of:
|
|
177
|
-
* - a named ref (`'@/member'`, or `{ schema: '@/member' }`) → resolved on
|
|
178
|
-
* disk by the build's data-schema resolver; its fields are lean-extracted.
|
|
179
|
-
* - an inline rich-form schema (`{ fields: [...] }`) → passed through
|
|
180
|
-
* normalized (drives the FormBlock editor UI + default application).
|
|
181
|
-
* - an inline full-format schema (`{ name, version, fields: {...} }`) or a
|
|
182
|
-
* bare field map (`{ field: {...} }`) → lean-extracted.
|
|
183
|
-
*
|
|
184
|
-
* Source-agnostic: the same `content.data` key may be filled by a fetched
|
|
185
|
-
* collection, a tagged code block, or an editor form — the schema (and its
|
|
186
|
-
* defaults) is identical, so there is one declaration surface. Returns the
|
|
187
|
-
* lean structure, or null when there's nothing to apply.
|
|
188
|
-
*
|
|
189
|
-
* @param {string|Object} value - The `data:` entry value
|
|
190
|
-
* @param {Object} dataSchemaMap - Resolved schemas keyed by ref
|
|
191
|
-
* @returns {Object|null}
|
|
192
|
-
*/
|
|
193
|
-
function leanDataSchema(value, dataSchemaMap) {
|
|
194
|
-
// Named ref → the resolved schema's fields. `'@std/article/*'` asks for whole records of it.
|
|
195
|
-
const { ref, whole } = dataRefOf(value)
|
|
196
|
-
if (ref) {
|
|
197
|
-
const resolved = dataSchemaMap[ref]
|
|
198
|
-
// ⭐ THE FIELDS OF WHAT THE COMPONENT RECEIVES (ruled 2026-09-27 [Diego]): a brief's fields at
|
|
199
|
-
// the top (`briefFieldMap`), or, for `'@x/y/*'`, the whole record as stored — one field per
|
|
200
|
-
// section, the brief's included (`wholeFieldMap`) — so a default lands where the record carries
|
|
201
|
-
// its field: `@std/article`'s `status` inside `body`. A section is filled only when the record
|
|
202
|
-
// holds it (`applySchemaToObject` recurses into what is there).
|
|
203
|
-
//
|
|
204
|
-
// ⛔ It was the merged `deliveredFields` until 2026-09-27 — the brief's fields beside the other
|
|
205
|
-
// sections' names, one map for two shapes; `flatRecordFields`, the retired flat form, until
|
|
206
|
-
// 2026-09-24; and before 2026-09-03 `resolved.fields`, which a sections-form schema does not
|
|
207
|
-
// have. `dataSchemaMap` holds each schema normalized in its authored form (`fields:` or
|
|
208
|
-
// `sections:`), and only these readers say what one record of it looks like.
|
|
209
|
-
const fields = whole ? wholeFieldMap(resolved) : briefFieldMap(resolved)
|
|
210
|
-
if (!fields) return null
|
|
211
|
-
const lean = extractSchemaFields(fields)
|
|
212
|
-
return Object.keys(lean).length > 0 ? lean : null
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
216
|
-
|
|
217
|
-
// Inline rich-form schema (fields: array) — drives FormBlock + defaults.
|
|
218
|
-
if (isRichSchema(value)) return normalizeRichSchema(value)
|
|
219
|
-
|
|
220
|
-
// Inline full-format schema or a bare field map.
|
|
221
|
-
const fields = isFullSchemaFormat(value) ? value.fields : value
|
|
222
|
-
const lean = extractSchemaFields(fields)
|
|
223
|
-
return Object.keys(lean).length > 0 ? lean : null
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
50
|
/**
|
|
228
51
|
* Extract param defaults from params object
|
|
229
52
|
*
|
|
@@ -246,27 +69,45 @@ function extractParamDefaults(params) {
|
|
|
246
69
|
return Object.keys(defaults).length > 0 ? defaults : null
|
|
247
70
|
}
|
|
248
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A section type's CSS variables as the runtime reads them: every declared name, with its
|
|
74
|
+
* `default` when it has one. `Block.mergeComponentVars` (`@uniweb/core`) emits each default
|
|
75
|
+
* on the section and takes a section's `vars:` value only for a name declared here, so a
|
|
76
|
+
* var with no default still needs its name. A label, a type, options and a description are
|
|
77
|
+
* the editor's. A shorthand value (`'card-gap': '1.5rem'`) is written as its `default`.
|
|
78
|
+
*
|
|
79
|
+
* ⛔ Not emitted until 2026-10-05, while core read it since 2026-03-02: a section type's
|
|
80
|
+
* `vars:` reached no page, and neither did a section's `vars:` for them.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} vars - the `vars:` of a meta.js
|
|
83
|
+
* @returns {Object|null} `{ <name>: { default? } }`, or null when none is declared
|
|
84
|
+
*/
|
|
85
|
+
function leanComponentVars(vars) {
|
|
86
|
+
if (!vars || typeof vars !== 'object' || Array.isArray(vars)) return null
|
|
87
|
+
const out = {}
|
|
88
|
+
for (const [name, config] of Object.entries(vars)) {
|
|
89
|
+
if (config !== null && typeof config === 'object' && !Array.isArray(config)) {
|
|
90
|
+
out[name] = config.default !== undefined ? { default: config.default } : {}
|
|
91
|
+
} else {
|
|
92
|
+
out[name] = config === null || config === undefined ? {} : { default: config }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return Object.keys(out).length > 0 ? out : null
|
|
96
|
+
}
|
|
97
|
+
|
|
249
98
|
/**
|
|
250
99
|
* Extract lean runtime schema from a full meta.js object
|
|
251
100
|
*
|
|
252
101
|
* @param {Object} fullMeta - The full meta.js default export
|
|
253
|
-
* @param {Object} [dataSchemaMap] - Resolved data schemas keyed by ref (from
|
|
254
|
-
* resolve-data-schema.js), used to lean-extract field defaults
|
|
255
|
-
* for each `data:` binding.
|
|
256
102
|
* @returns {Object|null} - Lean runtime schema or null if empty
|
|
257
103
|
*/
|
|
258
|
-
export function extractRuntimeSchema(fullMeta
|
|
104
|
+
export function extractRuntimeSchema(fullMeta) {
|
|
259
105
|
if (!fullMeta || typeof fullMeta !== 'object') {
|
|
260
106
|
return null
|
|
261
107
|
}
|
|
262
108
|
|
|
263
109
|
const runtime = {}
|
|
264
110
|
|
|
265
|
-
// Inset flag: signals this component is available for inline @ references
|
|
266
|
-
if (fullMeta.inset) {
|
|
267
|
-
runtime.inset = true
|
|
268
|
-
}
|
|
269
|
-
|
|
270
111
|
// Background opt-out: 'self' means the component renders its own background
|
|
271
112
|
// layer (solid colors, insets, effects), so the runtime skips its Background.
|
|
272
113
|
if (fullMeta.background) {
|
|
@@ -277,12 +118,12 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
277
118
|
// data: it maps each `content.data` key to its schema. A value is a named ref
|
|
278
119
|
// (`'@/member'`), an inline field map, or an inline rich-form (`{ fields: [...] }`,
|
|
279
120
|
// an editor form). Source-agnostic — the data may arrive by fetch, tagged code
|
|
280
|
-
// block, or editor form
|
|
121
|
+
// block, or editor form, and it reaches the component as it arrived.
|
|
281
122
|
//
|
|
282
|
-
// ⭐ Every key reaches the runtime
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
123
|
+
// ⭐ Every key reaches the runtime — `data` lists them with their refs, because the keys
|
|
124
|
+
// ARE what the section receives, and a key's ref is what a fetch of another name fills
|
|
125
|
+
// it by. Its shape stays in schema.json, for the editor. `data: false` declares nothing,
|
|
126
|
+
// as no `data:` does.
|
|
286
127
|
//
|
|
287
128
|
// ⭐ A concept block's key is written as its fence is — `'md:faq': 'Questions and
|
|
288
129
|
// answers'` — and lowered to the key a component reads, `faq`, with no schema: the value
|
|
@@ -308,11 +149,6 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
308
149
|
}
|
|
309
150
|
runtime.data = runtime.data || {}
|
|
310
151
|
runtime.data[key] = dataRef(value)
|
|
311
|
-
const lean = leanDataSchema(value, dataSchemaMap)
|
|
312
|
-
if (lean) {
|
|
313
|
-
runtime.schemas = runtime.schemas || {}
|
|
314
|
-
runtime.schemas[key] = lean
|
|
315
|
-
}
|
|
316
152
|
}
|
|
317
153
|
} else if (data !== undefined) {
|
|
318
154
|
throw new Error(
|
|
@@ -321,6 +157,11 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
321
157
|
)
|
|
322
158
|
}
|
|
323
159
|
|
|
160
|
+
const vars = leanComponentVars(fullMeta.vars)
|
|
161
|
+
if (vars) {
|
|
162
|
+
runtime.vars = vars
|
|
163
|
+
}
|
|
164
|
+
|
|
324
165
|
const paramsObj = fullMeta.params
|
|
325
166
|
const defaults = extractParamDefaults(paramsObj)
|
|
326
167
|
if (defaults) {
|
|
@@ -339,9 +180,6 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
339
180
|
runtime.initialState = fullMeta.initialState
|
|
340
181
|
}
|
|
341
182
|
|
|
342
|
-
// (Top-level `schemas:` is gone — inline field maps and rich-forms are now
|
|
343
|
-
// just `data:` entries with an inline value. See leanDataSchema.)
|
|
344
|
-
|
|
345
183
|
return Object.keys(runtime).length > 0 ? runtime : null
|
|
346
184
|
}
|
|
347
185
|
|
|
@@ -351,11 +189,11 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
351
189
|
* @param {Object} componentsMeta - Map of componentName -> meta.js content
|
|
352
190
|
* @returns {Object} - Map of componentName -> runtime schema (excludes null entries)
|
|
353
191
|
*/
|
|
354
|
-
export function extractAllRuntimeSchemas(componentsMeta
|
|
192
|
+
export function extractAllRuntimeSchemas(componentsMeta) {
|
|
355
193
|
const schemas = {}
|
|
356
194
|
|
|
357
195
|
for (const [name, meta] of Object.entries(componentsMeta)) {
|
|
358
|
-
const schema = extractRuntimeSchema(meta
|
|
196
|
+
const schema = extractRuntimeSchema(meta)
|
|
359
197
|
if (schema) {
|
|
360
198
|
schemas[name] = schema
|
|
361
199
|
}
|
|
@@ -368,7 +206,6 @@ export function extractAllRuntimeSchemas(componentsMeta, dataSchemaMap = {}) {
|
|
|
368
206
|
* Extract lean runtime schema for a layout from its full meta.js
|
|
369
207
|
*
|
|
370
208
|
* Layout runtime metadata:
|
|
371
|
-
* - areas: Array of area names this layout supports
|
|
372
209
|
* - transitions: View transition name overrides (the runtime auto-names areas;
|
|
373
210
|
* this overrides per region, or `false` opts the layout out)
|
|
374
211
|
* - defaults: Param default values
|
|
@@ -385,9 +222,8 @@ export function extractLayoutRuntimeSchema(fullMeta) {
|
|
|
385
222
|
|
|
386
223
|
const runtime = {}
|
|
387
224
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
225
|
+
// ⛔ No `areas`: the areas a page renders are its site's `layout/` folder
|
|
226
|
+
// (`Website.getLayoutAreas`), and a layout's list of them is for the editor.
|
|
391
227
|
|
|
392
228
|
// `false` is a value, not an absence: it is the documented way a layout opts
|
|
393
229
|
// out of per-area view transitions. The previous guard was
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -112,7 +112,7 @@ export function isContentBodyField(field) {
|
|
|
112
112
|
* @param {(ref: string) => string} [opts.resolveOptions] - maps an `options`
|
|
113
113
|
* (item_ref) ref to its full `@org/model/<section>` path. Falls back to
|
|
114
114
|
* `resolveName` (model only) when not supplied.
|
|
115
|
-
* @returns {Object} the declaration (`{ name, label?, plural?, description?,
|
|
115
|
+
* @returns {Object} the declaration (`{ name, label?, plural?, icon?, description?,
|
|
116
116
|
* source_locale?, linkable?, sections }`).
|
|
117
117
|
*/
|
|
118
118
|
export function toDataSchemaDeclaration(normalized, { name, resolveName, resolveOptions } = {}) {
|
|
@@ -137,6 +137,9 @@ export function toDataSchemaDeclaration(normalized, { name, resolveName, resolve
|
|
|
137
137
|
const decl = { name }
|
|
138
138
|
if (normalized.label) decl.label = normalized.label
|
|
139
139
|
if (normalized.plural) decl.plural = normalized.plural
|
|
140
|
+
// The type's icon — a framework icon name (`lu-user`), sent as written; the registry
|
|
141
|
+
// stores it verbatim and a consumer resolves it. Since 2026-10-05 [Diego].
|
|
142
|
+
if (normalized.icon) decl.icon = normalized.icon
|
|
140
143
|
if (normalized.description) decl.description = normalized.description
|
|
141
144
|
if (normalized.sourceLocale) decl.source_locale = normalized.sourceLocale
|
|
142
145
|
// ⛔ No `creatable_by`: retired 2026-09-23 — Models are open, and `@uniweb/schemas`
|
|
@@ -451,8 +454,10 @@ function lowerLeaf(field, resolve, optResolve, { multiple = false } = {}) {
|
|
|
451
454
|
if (Array.isArray(field.enum)) out.enum = field.enum
|
|
452
455
|
if (leafFormat) out.format = leafFormat
|
|
453
456
|
|
|
454
|
-
// `default
|
|
455
|
-
//
|
|
457
|
+
// No `default`: a named schema declares none (`resolve-data-schema.js` refuses one, since
|
|
458
|
+
// 2026-10-05). ⛔ This said it "rides in the foundation-schema blob (render / editor
|
|
459
|
+
// pre-fill)" until then — but that blob leaves `dataSchemas` out, so a named schema's
|
|
460
|
+
// default reached the runtime alone, and no editor.
|
|
456
461
|
return out
|
|
457
462
|
}
|
|
458
463
|
|