@uniweb/build 0.16.19 → 0.16.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 +8 -8
- package/src/index.js +1 -0
- package/src/resolve-data-schema.js +1 -0
- package/src/uwx/data-schema.js +56 -15
- package/src/uwx/registry-package.js +12 -3
- package/src/validate-data.js +91 -2
- package/src/vite-foundation-plugin.js +27 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.21",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,16 +59,16 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.35.3",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/schemas": "0.2.
|
|
62
|
+
"@uniweb/schemas": "0.2.9",
|
|
63
|
+
"@uniweb/projections": "0.2.5",
|
|
63
64
|
"@uniweb/theming": "0.1.15",
|
|
64
|
-
"@uniweb/content-writer": "0.3.3"
|
|
65
|
-
"@uniweb/projections": "0.2.5"
|
|
65
|
+
"@uniweb/content-writer": "0.3.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/schemas": "0.2.
|
|
69
|
-
"@uniweb/
|
|
70
|
-
"@uniweb/
|
|
71
|
-
"@uniweb/
|
|
68
|
+
"@uniweb/schemas": "0.2.9",
|
|
69
|
+
"@uniweb/content-reader": "1.2.2",
|
|
70
|
+
"@uniweb/runtime": "0.9.9",
|
|
71
|
+
"@uniweb/semantic-parser": "1.2.1"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/index.js
CHANGED
package/src/uwx/data-schema.js
CHANGED
|
@@ -152,11 +152,16 @@ function lowerSection(def, resolve, optResolve, path = '') {
|
|
|
152
152
|
if (def.brief === true) out.brief = true
|
|
153
153
|
// Display prose IS a section key — the registry stores it and keys it for
|
|
154
154
|
// translation as `section.<name>.label` / `.description` (confirmed 2026-08-05).
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
155
|
+
// A LEAF is stored differently: its `label`/`description` are accepted by the
|
|
156
|
+
// registry's parser and then dropped FROM THE FIELD DECLARATION, because a
|
|
157
|
+
// field declaration has no slot for prose — field labels live in translation
|
|
158
|
+
// rows keyed `section.<name>.field.<key>.label`. Whether the parser relocates
|
|
159
|
+
// our inline values into those rows (as it relocates `enum` into a `one_of`
|
|
160
|
+
// constraint) or discards them is not stated, and it is the difference between
|
|
161
|
+
// authored field prose reaching the app and not. We keep emitting it either
|
|
162
|
+
// way: it is accepted, so there is no failure mode, and relocation needs no
|
|
163
|
+
// producer change. Do not restate this as "leaf prose is lost" — that reading
|
|
164
|
+
// was asserted here once on the strength of the word "dropped" alone.
|
|
160
165
|
if (def.label) out.label = def.label
|
|
161
166
|
if (def.description) out.description = def.description
|
|
162
167
|
if (def.nestable) out.self_nesting = true
|
|
@@ -252,7 +257,7 @@ function lowerField(rawField, resolve, optResolve, path = '') {
|
|
|
252
257
|
...lowerSection(
|
|
253
258
|
{
|
|
254
259
|
kind: 'multi',
|
|
255
|
-
...
|
|
260
|
+
...sectionAttrsFromField(field),
|
|
256
261
|
// `translatable: false` is load-bearing, not tidiness: a string field is
|
|
257
262
|
// localized by default, and a localized key could differ per locale —
|
|
258
263
|
// which would destroy the identity the key exists to carry. The key is an
|
|
@@ -261,7 +266,13 @@ function lowerField(rawField, resolve, optResolve, path = '') {
|
|
|
261
266
|
[OPEN_MAP_KEY]: { type: 'string', required: true, translatable: false },
|
|
262
267
|
...value.fields
|
|
263
268
|
},
|
|
264
|
-
|
|
269
|
+
// The uniqueness rule is STRUCTURAL — it is what makes the map's key the
|
|
270
|
+
// row's identity — so it is prepended rather than assigned: an author's
|
|
271
|
+
// own constraints on this field add to it and can never replace it.
|
|
272
|
+
constraints: [
|
|
273
|
+
{ kind: 'unique_field', field: OPEN_MAP_KEY, scope: 'section' },
|
|
274
|
+
...(sectionAttrsFromField(field).constraints || [])
|
|
275
|
+
]
|
|
265
276
|
},
|
|
266
277
|
resolve,
|
|
267
278
|
optResolve,
|
|
@@ -271,7 +282,7 @@ function lowerField(rawField, resolve, optResolve, path = '') {
|
|
|
271
282
|
}
|
|
272
283
|
return {
|
|
273
284
|
type: 'section',
|
|
274
|
-
...lowerSection({ kind: 'single', ...
|
|
285
|
+
...lowerSection({ kind: 'single', ...sectionAttrsFromField(field), fields: field.fields }, resolve, optResolve, path)
|
|
275
286
|
}
|
|
276
287
|
}
|
|
277
288
|
if (type === 'array') {
|
|
@@ -279,7 +290,7 @@ function lowerField(rawField, resolve, optResolve, path = '') {
|
|
|
279
290
|
if (items && items.type === 'object') {
|
|
280
291
|
return {
|
|
281
292
|
type: 'section',
|
|
282
|
-
...lowerSection({ kind: 'multi', ...
|
|
293
|
+
...lowerSection({ kind: 'multi', ...sectionAttrsFromField(field), fields: items.fields }, resolve, optResolve, path)
|
|
283
294
|
}
|
|
284
295
|
}
|
|
285
296
|
// A multi-valued LEAF or REFERENCE. `normalizeField` split this field in two
|
|
@@ -289,9 +300,22 @@ function lowerField(rawField, resolve, optResolve, path = '') {
|
|
|
289
300
|
// to `items` — so rejoin the halves and lower them as one leaf carrying
|
|
290
301
|
// `multiple: true`. Reading only `items.type` here is what silently dropped
|
|
291
302
|
// both halves' attributes; the rejoin is the exact inverse of the split.
|
|
303
|
+
//
|
|
304
|
+
// An UNTYPED array (`items` omitted — the authoring format allows it) becomes
|
|
305
|
+
// `json`, not `string`. It used to invent `string`, which is a claim about the
|
|
306
|
+
// elements that the author never made and that is wrong the moment the list
|
|
307
|
+
// holds anything else: `@std/form`'s `enum` is documented as "bare strings
|
|
308
|
+
// AND/OR { value, label }", and it was reaching the registry declared as a
|
|
309
|
+
// list of strings. `json` is this vocabulary's word for an opaque structured
|
|
310
|
+
// value, which is exactly what an undeclared element type is. Declaring
|
|
311
|
+
// `items:` remains the way to say something stronger.
|
|
312
|
+
//
|
|
313
|
+
// Note the local checker is unaffected and still stricter: `validateItem`
|
|
314
|
+
// works on the IR, where the field is `type: array`, so it verifies the value
|
|
315
|
+
// IS a list. Only what the registry is told changes.
|
|
292
316
|
const { items: _items, ...collection } = field
|
|
293
317
|
return lowerLeaf(
|
|
294
|
-
{ ...collection, ...items, type: items ? items.type : '
|
|
318
|
+
{ ...collection, ...items, type: items ? items.type : 'json' },
|
|
295
319
|
resolve,
|
|
296
320
|
optResolve,
|
|
297
321
|
{ multiple: true }
|
|
@@ -379,14 +403,31 @@ function asField(def) {
|
|
|
379
403
|
}
|
|
380
404
|
|
|
381
405
|
// A nested section is authored as a FIELD (`{ type: object, description: … }`),
|
|
382
|
-
// but arrives on the wire as a section — so
|
|
383
|
-
// field declaration onto the section body, where
|
|
384
|
-
// Without this an authored `description:` on a
|
|
385
|
-
// over
|
|
386
|
-
|
|
406
|
+
// but arrives on the wire as a section — so the attributes that belong to a
|
|
407
|
+
// SECTION have to travel from the field declaration onto the section body, where
|
|
408
|
+
// the registry has a slot for them. Without this an authored `description:` on a
|
|
409
|
+
// nested object was dropped twice over (once by the normalizer, then again here),
|
|
410
|
+
// and `constraints:` never arrived at all.
|
|
411
|
+
//
|
|
412
|
+
// `constraints` is the load-bearing one. A list of records is normally authored
|
|
413
|
+
// as a field — `authors: { type: object, many: true }` — so until this existed,
|
|
414
|
+
// section rules were declarable only in the `sections:` form and unreachable in
|
|
415
|
+
// the shape that usually needs them. `min_items` is the motivating rule; note it
|
|
416
|
+
// is a WRITE guarantee ("a delete may not take the section below N"), never a
|
|
417
|
+
// render guarantee — a component still handles an empty list, because the same
|
|
418
|
+
// Model is renderable by a foundation that never saw the constraint.
|
|
419
|
+
function sectionAttrsFromField(field) {
|
|
387
420
|
const out = {}
|
|
388
421
|
if (field.label) out.label = field.label
|
|
389
422
|
if (field.description) out.description = field.description
|
|
423
|
+
if (Array.isArray(field.constraints) && field.constraints.length) out.constraints = field.constraints
|
|
424
|
+
// `tree` (→ `self_nesting`) and `append_only` describe how a list of records
|
|
425
|
+
// behaves, so they belong to the section too. Same silent-drop as `constraints`
|
|
426
|
+
// had: a list authored as a FIELD could not be a tree or append-only, while the
|
|
427
|
+
// identical thing in `sections:` form could. `self_nesting` is valid on a nested
|
|
428
|
+
// section, not only a top-level one (backend, 2026-08-05).
|
|
429
|
+
if (field.nestable) out.nestable = true
|
|
430
|
+
if (field.append_only) out.append_only = true
|
|
390
431
|
return out
|
|
391
432
|
}
|
|
392
433
|
|
|
@@ -48,6 +48,9 @@ try {
|
|
|
48
48
|
* @param {string} [params.scope] - org scope (`@acme` or `acme`) resolving `@/x` -> `@acme/x`.
|
|
49
49
|
* @param {Object} [params.exporter] - `{ tool, version, instance }` for the envelope.
|
|
50
50
|
* @param {string} [params.exportedAt] - ISO timestamp (default: now).
|
|
51
|
+
* @param {string} [params.runtime] - the `@uniweb/runtime` version this build
|
|
52
|
+
* linked against (its compatibility floor), from `dist/runtime-pin.json`.
|
|
53
|
+
* Rides in `info.runtime`; omitted when unknown.
|
|
51
54
|
* @param {string} [params.digest] - the foundation's content digest (`sha256:…`),
|
|
52
55
|
* computed by the CLI over what register ships (shipping-model.md §4.1). Rides
|
|
53
56
|
* in the foundation-schema entity's `info.digest`; the backend stores it
|
|
@@ -55,7 +58,7 @@ try {
|
|
|
55
58
|
* can detect "code changed since release" with no local state.
|
|
56
59
|
* @returns {Object} the `.uwx` document (uwx/1; entities, names only, no uuids).
|
|
57
60
|
*/
|
|
58
|
-
export function buildRegistryPackage({ schema, foundationDir, scope, exporter, exportedAt, digest } = {}) {
|
|
61
|
+
export function buildRegistryPackage({ schema, foundationDir, scope, exporter, exportedAt, digest, runtime } = {}) {
|
|
59
62
|
const self = schema?._self
|
|
60
63
|
if (!self || !self.name || !self.version) {
|
|
61
64
|
throw new Error('buildRegistryPackage: schema._self with name + version is required')
|
|
@@ -70,7 +73,7 @@ export function buildRegistryPackage({ schema, foundationDir, scope, exporter, e
|
|
|
70
73
|
|
|
71
74
|
const foundationEntity = {
|
|
72
75
|
model: FOUNDATION_SCHEMA,
|
|
73
|
-
info: buildInfo(self, org, digest),
|
|
76
|
+
info: buildInfo(self, org, digest, runtime),
|
|
74
77
|
schema: buildSchemaBlob(schema),
|
|
75
78
|
i18n: { locales: loadI18nLocales(foundationDir) },
|
|
76
79
|
'data-schemas': { refs: buildRefs(dataSchemas, scoped) },
|
|
@@ -144,12 +147,18 @@ function wrapEntities(entities, exporter, exportedAt) {
|
|
|
144
147
|
// Identity card — decomposed so it's readable without opening the blob. The
|
|
145
148
|
// optional `digest` (sha256:…) is the foundation's content fingerprint; the
|
|
146
149
|
// backend stores it opaque and returns it on the foundation-latest read.
|
|
147
|
-
function buildInfo(self, org, digest) {
|
|
150
|
+
function buildInfo(self, org, digest, runtime) {
|
|
148
151
|
// Scope a bare foundation name (`src` -> `@acme/src`); leave an already-scoped name.
|
|
149
152
|
const name = org && !String(self.name).startsWith('@') ? `@${org}/${self.name}` : self.name
|
|
150
153
|
const info = { name, version: self.version, role: self.role || 'foundation' }
|
|
151
154
|
if (self.description !== undefined) info.description = self.description
|
|
152
155
|
if (digest) info.digest = digest
|
|
156
|
+
// The compatibility FLOOR this build links against (dist/runtime-pin.json).
|
|
157
|
+
// Omitted when unknown — and a consumer must read the omission as UNKNOWN
|
|
158
|
+
// rather than unconstrained, since a floor nobody stated cannot be shown to
|
|
159
|
+
// be satisfied. Same lift as `digest`: stated by the producer, opaque to the
|
|
160
|
+
// backend, acted on by whoever resolves a whole site.
|
|
161
|
+
if (runtime) info.runtime = runtime
|
|
153
162
|
return info
|
|
154
163
|
}
|
|
155
164
|
|
package/src/validate-data.js
CHANGED
|
@@ -29,7 +29,7 @@ import { join, resolve, basename } from 'node:path'
|
|
|
29
29
|
import yaml from 'js-yaml'
|
|
30
30
|
import { collectionNameFromUrl } from '@uniweb/core'
|
|
31
31
|
|
|
32
|
-
import { validateItem, isStaticallyCheckable } from '@uniweb/schemas/conform'
|
|
32
|
+
import { validateItem, isStaticallyCheckable, validateBound } from '@uniweb/schemas/conform'
|
|
33
33
|
import { validateAndNormalizeSchema } from './resolve-data-schema.js'
|
|
34
34
|
|
|
35
35
|
// The pure checker, re-exported so `@uniweb/build/validate` stays the one import
|
|
@@ -173,6 +173,13 @@ export async function validateDataInputs({ siteRoot, foundationPath }) {
|
|
|
173
173
|
for (const ref of concepts.schemas) schemasSeen.add(ref)
|
|
174
174
|
recordCount += concepts.checked
|
|
175
175
|
|
|
176
|
+
// Pass 4 — tagged data blocks, which join by the component's OWN binding.
|
|
177
|
+
const blocks = validateTaggedDataBlocks(site, foundation, dataSchemas)
|
|
178
|
+
violations.push(...blocks.violations)
|
|
179
|
+
deferred.push(...blocks.deferred)
|
|
180
|
+
for (const ref of blocks.schemas) schemasSeen.add(ref)
|
|
181
|
+
recordCount += blocks.checked
|
|
182
|
+
|
|
176
183
|
return {
|
|
177
184
|
violations,
|
|
178
185
|
deferred,
|
|
@@ -307,11 +314,15 @@ export async function validateConceptBlocks(site) {
|
|
|
307
314
|
|
|
308
315
|
/** Every concept block in a doc, including any nested inside a container. */
|
|
309
316
|
function conceptBlockNodes(doc) {
|
|
317
|
+
return nodesOfType(doc, 'concept_block')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function nodesOfType(doc, type) {
|
|
310
321
|
const out = []
|
|
311
322
|
const walk = (nodes) => {
|
|
312
323
|
for (const node of nodes || []) {
|
|
313
324
|
if (!node) continue
|
|
314
|
-
if (node.type ===
|
|
325
|
+
if (node.type === type) out.push(node)
|
|
315
326
|
else if (Array.isArray(node.content)) walk(node.content)
|
|
316
327
|
}
|
|
317
328
|
}
|
|
@@ -319,6 +330,84 @@ function conceptBlockNodes(doc) {
|
|
|
319
330
|
return out
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Check each ```` ```yaml:<tag> ```` / ```` ```json:<tag> ```` data block against
|
|
335
|
+
* the schema the section's own component BOUND to that key.
|
|
336
|
+
*
|
|
337
|
+
* This is the pass that closes an odd hole: a component declares
|
|
338
|
+
* `data: { form: '@std/form' }`, an author writes a ```` ```yaml:form ```` block,
|
|
339
|
+
* and until now **nothing checked one against the other**. The join walked
|
|
340
|
+
* `section.fetch` — collections and fetches — so a schema bound to a key that a
|
|
341
|
+
* tagged block fills was never applied to anything. `@std/form` existed for
|
|
342
|
+
* exactly this and had never run outside its own contract test.
|
|
343
|
+
*
|
|
344
|
+
* Unlike concept blocks (pass 3), the join here is NOT by convention. A concept
|
|
345
|
+
* block resolves `md:faq` → `@std/faq` mechanically, which is why that pass must
|
|
346
|
+
* stay silent when no such schema exists. This one uses the binding the component
|
|
347
|
+
* actually declared, so there is no naming rule and no registry — a tag nobody
|
|
348
|
+
* bound is simply not governed, and says nothing.
|
|
349
|
+
*
|
|
350
|
+
* The value needs no parsing: a tagged fence lands as a `dataBlock` node with its
|
|
351
|
+
* parsed value already on `attrs.data`, and a body that FAILED to parse never
|
|
352
|
+
* becomes one (it falls back to `codeBlock`), so a malformed block cannot reach
|
|
353
|
+
* here and be misreported as a schema violation.
|
|
354
|
+
*
|
|
355
|
+
* Uses `validateBound` rather than `validateItem` because a block's value may be
|
|
356
|
+
* a record OR a list — ```` ```yaml:nav ```` is a bare array. That dispatch is the
|
|
357
|
+
* reason root-list conformance had to land first.
|
|
358
|
+
*
|
|
359
|
+
* @param {Object} site - collected site content
|
|
360
|
+
* @param {Object} foundation - the built foundation schema (type → { data })
|
|
361
|
+
* @param {Object} dataSchemas - normalized schemas keyed by ref
|
|
362
|
+
* @returns {{ violations: Array, schemas: Set<string>, checked: number, deferred: Array }}
|
|
363
|
+
*/
|
|
364
|
+
export function validateTaggedDataBlocks(site, foundation, dataSchemas) {
|
|
365
|
+
const violations = []
|
|
366
|
+
const schemas = new Set()
|
|
367
|
+
const deferred = []
|
|
368
|
+
let checked = 0
|
|
369
|
+
|
|
370
|
+
for (const page of site?.pages || []) {
|
|
371
|
+
walkSections(page.sections || [], (section) => {
|
|
372
|
+
const type = section.type
|
|
373
|
+
const bindings = type && foundation?.[type]?.data
|
|
374
|
+
if (!bindings || typeof bindings !== 'object') return
|
|
375
|
+
|
|
376
|
+
for (const node of nodesOfType(section.content, 'dataBlock')) {
|
|
377
|
+
const tag = node.attrs?.tag
|
|
378
|
+
if (!tag) continue
|
|
379
|
+
|
|
380
|
+
const binding = bindings[tag]
|
|
381
|
+
if (binding === undefined) continue // this key is not governed — say nothing
|
|
382
|
+
|
|
383
|
+
// A binding is a named ref, or an inline schema. Only a ref resolves to a
|
|
384
|
+
// normalized schema here; an inline one is reported rather than guessed at.
|
|
385
|
+
const ref = typeof binding === 'string' ? binding : binding?.schema
|
|
386
|
+
if (typeof ref !== 'string') {
|
|
387
|
+
deferred.push({ route: page.route, section: type, key: tag, reason: 'inline schema on the binding' })
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
const schema = dataSchemas?.[ref]
|
|
391
|
+
if (!schema) continue // unresolved ref — the build reports that on its own
|
|
392
|
+
|
|
393
|
+
schemas.add(ref)
|
|
394
|
+
checked++
|
|
395
|
+
for (const finding of validateBound(schema, node.attrs?.data)) {
|
|
396
|
+
violations.push({
|
|
397
|
+
file: `${page.route || '/'} › ${type} › ${node.attrs?.language || 'yaml'}:${tag}`,
|
|
398
|
+
schema: ref,
|
|
399
|
+
item: `data.${tag}`,
|
|
400
|
+
users: [{ route: page.route, section: type, key: tag }],
|
|
401
|
+
...finding,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { violations, schemas, checked, deferred }
|
|
409
|
+
}
|
|
410
|
+
|
|
322
411
|
/** `parseContent`, or null when the parser is not installed. */
|
|
323
412
|
async function loadSemanticParser() {
|
|
324
413
|
try {
|
|
@@ -45,9 +45,24 @@ let _buildingSSRBundle = false
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Emit dist/runtime-pin.json declaring the @uniweb/runtime version this
|
|
48
|
-
* foundation was built against.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* foundation was built against.
|
|
49
|
+
*
|
|
50
|
+
* ⛔ **NOTHING ENFORCES IT.** `uniweb register` reads the pin and carries it
|
|
51
|
+
* with the foundation (as `info.runtime`), so the floor reaches a party that
|
|
52
|
+
* could act on it — but no lane checks it yet. This header previously said a
|
|
53
|
+
* server-side isolate read the pin to pick which runtime to side-load, and that
|
|
54
|
+
* a "semver resolver" applied the policy. Neither existed, and for a while the
|
|
55
|
+
* pin was written and read by nothing at all. The wrong version had reached
|
|
56
|
+
* four documentation surfaces before anyone checked it against the consumers,
|
|
57
|
+
* which is the cost of describing a design as though it had shipped.
|
|
58
|
+
*
|
|
59
|
+
* The pin is a **compatibility floor**, not a selector, and it cannot be a
|
|
60
|
+
* selector: a site loads a primary foundation plus N extensions, each emitting
|
|
61
|
+
* its own pin, while a site has exactly one runtime — pins are plural, the
|
|
62
|
+
* choice is singular. The selector is `site.yml::runtime`. The pin's use is
|
|
63
|
+
* VALIDATION — is a site's runtime at or above max() of every loaded
|
|
64
|
+
* foundation's floor? — which belongs wherever all of those foundations are
|
|
65
|
+
* held, not here: this build sees one foundation.
|
|
51
66
|
*
|
|
52
67
|
* Reads the resolved version from the foundation's node_modules/@uniweb/
|
|
53
68
|
* runtime/package.json so the pin reflects what was actually linked at
|
|
@@ -56,13 +71,13 @@ let _buildingSSRBundle = false
|
|
|
56
71
|
*
|
|
57
72
|
* Silently no-ops when @uniweb/runtime isn't resolvable (e.g., the
|
|
58
73
|
* foundation depends on the runtime via a workspace alias that puts it
|
|
59
|
-
* elsewhere).
|
|
60
|
-
*
|
|
61
|
-
*
|
|
74
|
+
* elsewhere). Nothing breaks without it — but note the absence means the
|
|
75
|
+
* foundation states NO floor, and "unknown" is not "unconstrained": a floor
|
|
76
|
+
* nobody declared cannot be shown to be satisfied.
|
|
62
77
|
*
|
|
63
|
-
* Optional foundation-author override: a `uniweb.runtimePolicy` field
|
|
64
|
-
*
|
|
65
|
-
*
|
|
78
|
+
* Optional foundation-author override: a `uniweb.runtimePolicy` field in the
|
|
79
|
+
* foundation's own package.json is recorded alongside the runtime version,
|
|
80
|
+
* declaring the author's intent for the validation above.
|
|
66
81
|
*
|
|
67
82
|
* @param {string} outDir - dist/ directory to write to.
|
|
68
83
|
* @param {string} projectRoot - foundation project root (where package.json lives).
|
|
@@ -555,11 +570,9 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
555
570
|
// site build's theme.css — this is harmless redundancy there.
|
|
556
571
|
await emitFoundationVarsCss(outDir, schema)
|
|
557
572
|
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
// window already have the pin and start using the split-bundle path
|
|
562
|
-
// automatically once the edge is updated.
|
|
573
|
+
// Record which @uniweb/runtime this build linked against. A compatibility
|
|
574
|
+
// floor for a validation step that does not exist yet, and read by nothing
|
|
575
|
+
// today — see emitRuntimePin's header before assuming otherwise.
|
|
563
576
|
await emitRuntimePin(outDir, resolvedRoot)
|
|
564
577
|
|
|
565
578
|
// Emit dist/entry-ssr.js — the single-file SSR twin of the (code-split)
|