@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.75.1",
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/projections": "^0.7.13",
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.28.10"
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.28.10",
78
- "@uniweb/core": "^0.36.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 any file the entry is generated
247
- // from — a data schema under `schemas/` is `.yml` or `.json` as often as `.js`
248
- // (`shouldRegenerateForFile`)
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') ||
@@ -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
- * - `schemas` - field defaults for those keys
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 { collectSchemaRefs, buildDataSchemaMap, SCHEMA_EXTENSIONS } from './resolve-data-schema.js'
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 (areas, transitions, defaults)
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
- // Resolve the data schemas referenced by section bindings, then extract
315
- // per-component runtime metadata (which lean-extracts field defaults from
316
- // the resolved schemas into meta.schemas[<key>]).
317
- // ⛔ Components only, unlike `schema.js`, which also passes the foundation's
318
- // `main.js` `data:`. Not an oversight: this map feeds `extractAllRuntimeSchemas`,
319
- // and `extractRuntimeSchema` reads `fullMeta.data` — per component. A
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
- // ⭐ The foundation's data schemas — `schemas/<name>.<ext>` and `schemas.config.js`.
523
- // The entry's `meta` carries each declared `data:` key with its schema's field
524
- // defaults, and a `@/name` ref whose file is missing stops entry generation. A
525
- // section receives only the keys its entry declares (2026-09-14), so an entry that
526
- // falls behind withholds data: a `data:` key declaring `@/member` before
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 and no field defaults from
98
- * `applySchemas`, with no build error. It bit nobody only because every
99
- * foundation-tier declaration in the templates is ref-less.
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 validateAndNormalizeSchema(await loadSchemaFile(file), ref)
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 validateAndNormalizeSchema(system, ref)
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 validateAndNormalizeSchema(await loadSchemaFile(file), ref)
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 validateAndNormalizeSchema(await loadSchemaFile(file), ref)
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 validateAndNormalizeSchema(schema, ref)
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 both schema discovery and the runtime-schema build go
228
- // through, so editor schema.json, runtime defaults, and `uniweb validate` all
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}`] = validateAndNormalizeSchema(schema, `@/${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}`] = validateAndNormalizeSchema(await loadSchemaFile(join(dir, file)), `@/${name}`)
365
+ out[`@/${name}`] = normalizeNamedSchema(await loadSchemaFile(join(dir, file)), `@/${name}`)
313
366
  }
314
367
  return out
315
368
  }
@@ -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
- * The runtime schema is optimized for size and contains only what's
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
- * - schemas: { <key>: <lean fields> } — field defaults for the declared keys that have any
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, dataSchemaMap = {}) {
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; the schema and its defaults are identical.
121
+ // block, or editor form, and it reaches the component as it arrived.
281
122
  //
282
- // ⭐ Every key reaches the runtime, a key with no fields included — `data` lists them
283
- // with their refs, because the keys ARE what the section receives, and a key's ref is
284
- // what a fetch of another name fills it by. `schemas` carries field defaults for the
285
- // keys that have fields. `data: false` declares nothing, as no `data:` does.
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, dataSchemaMap = {}) {
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, dataSchemaMap)
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
- if (fullMeta.areas && Array.isArray(fullMeta.areas)) {
389
- runtime.areas = fullMeta.areas
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
@@ -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` is intentionally NOT emitted — it rides in the foundation-schema
455
- // blob (render / editor pre-fill), not the content type.
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