@uniweb/build 0.70.0 → 0.71.0
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/site/content-collector.js +28 -13
- package/src/uwx/same-content.js +1 -1
- package/src/uwx/site-diff.js +2 -2
- package/src/uwx/site-project.js +14 -52
- package/src/uwx/site.js +11 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
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/content-reader": "^1.2.7",
|
|
61
60
|
"@uniweb/content-writer": "^0.3.4",
|
|
62
61
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
63
|
-
"@uniweb/
|
|
64
|
-
"@uniweb/
|
|
65
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/projections": "^0.7.10",
|
|
63
|
+
"@uniweb/theming": "^0.1.16",
|
|
64
|
+
"@uniweb/content-reader": "^1.2.7",
|
|
65
|
+
"@uniweb/schemas": "^0.9.1"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.28.
|
|
68
|
+
"@uniweb/runtime": "^0.28.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/core": "^0.
|
|
78
|
-
"@uniweb/runtime": "^0.28.
|
|
77
|
+
"@uniweb/core": "^0.34.0",
|
|
78
|
+
"@uniweb/runtime": "^0.28.1"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
|
@@ -749,8 +749,13 @@ function prettifySlug(slug) {
|
|
|
749
749
|
*
|
|
750
750
|
* A node with no sections of its own (a group/container node, a redirect stub)
|
|
751
751
|
* has nothing to humanize from, so it shows the identifier the author actually
|
|
752
|
-
* wrote — `v1` stays `v1`. A node with content falls back to
|
|
753
|
-
* and only then to a prettified slug.
|
|
752
|
+
* wrote — `v1` stays `v1`. A node with content falls back to the opening H1 of
|
|
753
|
+
* its first section that publishes, and only then to a prettified slug.
|
|
754
|
+
*
|
|
755
|
+
* ⭐ "That publishes": a `hidden: true` section is a draft, so its heading never
|
|
756
|
+
* titles the page — in dev as in a published build, so the title does not change
|
|
757
|
+
* between them, and as a backend's publish does [2026-09-28]. A node whose sections
|
|
758
|
+
* are all hidden takes the prettified slug.
|
|
754
759
|
*
|
|
755
760
|
* The runtime has no fallback of its own: an empty title renders empty. The
|
|
756
761
|
* fill belongs here, at the producer, because `title` also feeds `<title>`,
|
|
@@ -765,7 +770,8 @@ function prettifySlug(slug) {
|
|
|
765
770
|
function resolveDisplayTitle(declaredTitle, segment, sections) {
|
|
766
771
|
if (declaredTitle) return declaredTitle
|
|
767
772
|
if (!sections?.length) return segment
|
|
768
|
-
|
|
773
|
+
const first = sections.find((section) => section?.hidden !== true)
|
|
774
|
+
return extractH1(first?.content) || prettifySlug(segment)
|
|
769
775
|
}
|
|
770
776
|
|
|
771
777
|
// ⭐ The rule now lives in `utils/numeric-prefix.js` — `records/folder.yml` needs the
|
|
@@ -957,7 +963,9 @@ async function processFileAsPage(filePath, fileName, siteRoot, parentRoute) {
|
|
|
957
963
|
version: null,
|
|
958
964
|
versionMeta: null,
|
|
959
965
|
versionScope: null,
|
|
960
|
-
hidden:
|
|
966
|
+
// The file IS the page, so its `hidden: true` drafts the page — left out of a published
|
|
967
|
+
// build, kept in dev — rather than leaving a page with no section (2026-09-28).
|
|
968
|
+
hidden: section.hidden === true,
|
|
961
969
|
hideIn: [],
|
|
962
970
|
layout: {},
|
|
963
971
|
seo: {
|
|
@@ -2806,7 +2814,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2806
2814
|
// Dev keeps them so drafts stay previewable.
|
|
2807
2815
|
pages: dropUnpublished ? dropUnpublishedPages(pages).map(withoutHiddenSections) : pages,
|
|
2808
2816
|
// Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
|
|
2809
|
-
layouts: dropUnpublished ?
|
|
2817
|
+
layouts: dropUnpublished ? withoutHiddenAreas(layouts) : layouts,
|
|
2810
2818
|
// ⭐ THE SAME REACHABILITY AXIS, for the 404 slot. `hidden: true` means DRAFT —
|
|
2811
2819
|
// "excluded from the published site" (`docs/reference/page-configuration.md`);
|
|
2812
2820
|
// `hideIn: ['*']` is the control for "routed but in no nav".
|
|
@@ -2836,16 +2844,23 @@ function withoutHiddenSections(page) {
|
|
|
2836
2844
|
return { ...page, sections: dropHiddenSections(page.sections) }
|
|
2837
2845
|
}
|
|
2838
2846
|
|
|
2839
|
-
//
|
|
2840
|
-
|
|
2847
|
+
// Every layout's areas without their hidden sections — and without an area whose sections
|
|
2848
|
+
// were all hidden: drafting every section of an area drafts the area, so a layout gets no area
|
|
2849
|
+
// there rather than an empty one (a backend's publish does the same).
|
|
2850
|
+
function withoutHiddenAreas(layouts) {
|
|
2841
2851
|
if (!layouts || typeof layouts !== 'object') return layouts
|
|
2842
2852
|
return Object.fromEntries(
|
|
2843
|
-
Object.entries(layouts).map(([name, areas]) =>
|
|
2844
|
-
name,
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2853
|
+
Object.entries(layouts).map(([name, areas]) => {
|
|
2854
|
+
if (!areas || typeof areas !== 'object') return [name, areas]
|
|
2855
|
+
const kept = {}
|
|
2856
|
+
for (const [area, page] of Object.entries(areas)) {
|
|
2857
|
+
const shown = withoutHiddenSections(page)
|
|
2858
|
+
const hadSections = Array.isArray(page?.sections) && page.sections.length > 0
|
|
2859
|
+
if (hadSections && shown.sections.length === 0) continue
|
|
2860
|
+
kept[area] = shown
|
|
2861
|
+
}
|
|
2862
|
+
return [name, kept]
|
|
2863
|
+
})
|
|
2849
2864
|
)
|
|
2850
2865
|
}
|
|
2851
2866
|
|
package/src/uwx/same-content.js
CHANGED
|
@@ -32,7 +32,7 @@ export function sameMarkdownDocument(markdown, doc) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// A document without the attributes that only restate a default: an inset's `embedKind: 'visual'`,
|
|
35
|
-
// which the parser writes and the pull's re-inlining
|
|
35
|
+
// which the parser writes and the pull's re-inlining (retired 2026-09-28) left out — so a body holding
|
|
36
36
|
// an inset never compared as unchanged (measured 2026-09-26 on the `marketing` template's hero).
|
|
37
37
|
function withoutDefaults(doc) {
|
|
38
38
|
return JSON.parse(JSON.stringify(doc), (_, v) => {
|
package/src/uwx/site-diff.js
CHANGED
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
// It needs TWO bases, and that is the non-obvious part.
|
|
30
30
|
//
|
|
31
31
|
// Our document and the backend's are not byte-comparable renderings of the same
|
|
32
|
-
// unit: the backend's copy carries fields we don't emit (
|
|
33
|
-
//
|
|
32
|
+
// unit: the backend's copy carries fields we don't emit (its own `$uuid`, for
|
|
33
|
+
// one) and serializes in its own key order. So a hash
|
|
34
34
|
// taken on our side and one taken on theirs differ for a unit NEITHER side touched.
|
|
35
35
|
// A single base therefore validates exactly one comparison and silently corrupts
|
|
36
36
|
// the other — with a local-representation base every unit looks "changed
|
package/src/uwx/site-project.js
CHANGED
|
@@ -383,50 +383,12 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
|
|
|
383
383
|
// Section records → section .md files
|
|
384
384
|
// ---------------------------------------------------------------------------
|
|
385
385
|
|
|
386
|
-
/**
|
|
387
|
-
* Re-inline a section's extracted insets back into its ProseMirror content —
|
|
388
|
-
* the exact inverse of content-collector's `extractInsets`. The producer pulls
|
|
389
|
-
* each `{params}` ref out of the body into an `insets[]` array
|
|
390
|
-
* and leaves an `inset_placeholder` behind; content-writer only serializes
|
|
391
|
-
* `inset_ref` nodes, so we restore them before serializing or the inset would
|
|
392
|
-
* be reported as unmappable and dropped.
|
|
393
|
-
*
|
|
394
|
-
* @param {object} content - the section's ProseMirror document (placeholders in)
|
|
395
|
-
* @param {Array} insets - `[{ refId, type, params, title, embedKind }]`
|
|
396
|
-
* @returns {object} a content document with `inset_ref` nodes restored
|
|
397
|
-
*/
|
|
398
|
-
function reinlineInsets(content, insets) {
|
|
399
|
-
if (!content || !Array.isArray(content.content) || !Array.isArray(insets) || insets.length === 0) {
|
|
400
|
-
return content
|
|
401
|
-
}
|
|
402
|
-
const byRef = new Map(insets.map((i) => [i.refId, i]))
|
|
403
|
-
|
|
404
|
-
const visit = (nodes) =>
|
|
405
|
-
nodes.map((node) => {
|
|
406
|
-
if (!node) return node
|
|
407
|
-
if (node.type === 'inset_placeholder') {
|
|
408
|
-
const inset = byRef.get(node.attrs?.refId)
|
|
409
|
-
if (!inset) return node // no match → leave the placeholder (the guard reports it)
|
|
410
|
-
const attrs = { component: inset.type, ...(inset.params || {}) }
|
|
411
|
-
if (inset.title != null) attrs.alt = inset.title
|
|
412
|
-
// `visual` is the extractor's default — omit it so the projected markdown
|
|
413
|
-
// doesn't gain a spurious `{embedKind=visual}` the source never had.
|
|
414
|
-
if (inset.embedKind && inset.embedKind !== 'visual') attrs.embedKind = inset.embedKind
|
|
415
|
-
return { type: 'inset_ref', attrs }
|
|
416
|
-
}
|
|
417
|
-
if (Array.isArray(node.content)) return { ...node, content: visit(node.content) }
|
|
418
|
-
return node
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
return { ...content, content: visit(content.content) }
|
|
422
|
-
}
|
|
423
|
-
|
|
424
386
|
/**
|
|
425
387
|
* Project one section `$`-record (from `page_sections` / `layout_sections`) to a
|
|
426
388
|
* section `.md` file — the inverse of site.js `mapSectionData`. Frontmatter is
|
|
427
|
-
* `type` + the flat `params`
|
|
428
|
-
* `fetch`
|
|
429
|
-
*
|
|
389
|
+
* `type` + `hidden` + the flat `params` (`background` and `theme` among them) +
|
|
390
|
+
* the data declaration (`params.fetch`, else the `fetch` field) + `id` (`stable_id`);
|
|
391
|
+
* the body is the section's content serialized to markdown. Idempotent.
|
|
430
392
|
*
|
|
431
393
|
* Note: `$children` (a section's child sections) are NOT written here — the page
|
|
432
394
|
* walk (`pageSectionsToFiles`) writes each to its own file and nests it under its
|
|
@@ -444,7 +406,10 @@ function reinlineInsets(content, insets) {
|
|
|
444
406
|
export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, freeformRelPath, freeformCandidates = null, writeId = true, context = null }) {
|
|
445
407
|
// ⛔ A stored `preset` or `input` is not written back: neither has an effect, and the build
|
|
446
408
|
// would only warn about it (2026-09-27).
|
|
447
|
-
|
|
409
|
+
// ⛔ No `background` / `theme_override` / `insets`: the site-content Model dropped all three
|
|
410
|
+
// (2026-09-28), with a stored background and theme copied into `params`. Until then a field
|
|
411
|
+
// filled in where `params` lacked the key, and a stored `insets[]` was re-inlined into the body.
|
|
412
|
+
const { type, stable_id, hidden, params, content, fetch } = record || {}
|
|
448
413
|
|
|
449
414
|
// A localized `content` field unwraps to the source-locale doc for the body; its
|
|
450
415
|
// target-locale structural maps are captured into the locales/ collector, and any
|
|
@@ -452,7 +417,7 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
|
|
|
452
417
|
// locales/freeform/. A bare doc (source-only / pre-localization) passes through.
|
|
453
418
|
const sourceContent = unwrapLocalizedContent(content, sourceLocale, collector, freeformRelPath, freeformCandidates, context)
|
|
454
419
|
|
|
455
|
-
const { hidden: paramHidden, ...paramsRest } = params && typeof params === 'object' ? params : {}
|
|
420
|
+
const { hidden: paramHidden, fetch: paramFetch, ...paramsRest } = params && typeof params === 'object' ? params : {}
|
|
456
421
|
const frontmatter = {}
|
|
457
422
|
if (type !== undefined) frontmatter.type = type
|
|
458
423
|
// ⭐ `hidden:` says what the store says: written for a hidden section, and removed from the
|
|
@@ -461,25 +426,22 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
|
|
|
461
426
|
// `params`, so one stored there counts when the field is absent.
|
|
462
427
|
frontmatter.hidden = (typeof hidden === 'boolean' ? hidden : paramHidden === true) ? true : undefined
|
|
463
428
|
Object.assign(frontmatter, paramsRest)
|
|
464
|
-
// ⚠️ A stored `background` / `theme_override` field wins over the same key in `params` while
|
|
465
|
-
// one is stored: an editor may still write the fields rather than `params` (2026-09-28;
|
|
466
|
-
// uwx-format.md § A page section's fields, Order).
|
|
467
|
-
if (background !== undefined) frontmatter.background = background
|
|
468
|
-
if (theme_override !== undefined) frontmatter.theme = theme_override
|
|
469
429
|
// Invert the build's resolution rather than copy it — see fetch-shapes.js. A new
|
|
470
430
|
// file gets `query:` for a declaration of names alone; a section that declares
|
|
471
431
|
// its data locally keeps it (`writeSectionFile`, the declaration keys).
|
|
472
|
-
|
|
473
|
-
|
|
432
|
+
// ⭐ From `params.fetch` when present — where a section carries its data since 2026-09-28 —
|
|
433
|
+
// else the stored field.
|
|
434
|
+
const ownFetch = paramFetch !== undefined ? paramFetch : fetch
|
|
435
|
+
if (ownFetch !== undefined) {
|
|
436
|
+
const { key, value } = authorableDeclaration(ownFetch)
|
|
474
437
|
frontmatter[key] = value
|
|
475
438
|
}
|
|
476
439
|
// `id:` only where the file's name does not already give it (`writeId`, `pageSectionsToFiles`).
|
|
477
440
|
if (stable_id !== undefined && writeId) frontmatter.id = stable_id
|
|
478
441
|
|
|
479
|
-
const body = insets ? reinlineInsets(sourceContent, insets) : sourceContent
|
|
480
442
|
// ⛔ Not `type: Content` into a file that names no type: it is what the push sent for it, and
|
|
481
443
|
// absent, the foundation's `defaultSection` renders it. Until 2026-09-26 a pull wrote it back.
|
|
482
|
-
return writeSectionFile({ filePath, content:
|
|
444
|
+
return writeSectionFile({ filePath, content: sourceContent, params: frontmatter, implied: { type: FILLED_SECTION_TYPE } })
|
|
483
445
|
}
|
|
484
446
|
|
|
485
447
|
// ---------------------------------------------------------------------------
|
package/src/uwx/site.js
CHANGED
|
@@ -302,20 +302,25 @@ function localizePageMeta(page, route, sourceLocale, translations) {
|
|
|
302
302
|
|
|
303
303
|
function mapSectionData(section) {
|
|
304
304
|
const params = { ...section.params }
|
|
305
|
+
// ⭐ The section's own data rides in `params.fetch` [Diego, 2026-09-28] — nothing on a store's
|
|
306
|
+
// side acts on it by name. ⚠️ Also sent as the `fetch` field it had until then, until an
|
|
307
|
+
// editor's reader looks in `params.fetch` (uwx-format.md § A page section's fields).
|
|
308
|
+
if (section.fetch) params.fetch = section.fetch
|
|
305
309
|
const data = { type: section.type || FILLED_SECTION_TYPE } // entity type requires `type`
|
|
306
310
|
setIf(data, 'stable_id', section.stableId ?? undefined)
|
|
307
311
|
// ⭐ A draft section — the store's own `hidden`, which an editor also writes [2026-09-28].
|
|
308
312
|
if (section.hidden === true) data.hidden = true
|
|
309
313
|
// No `preset` or `input`: nothing read either, and the collector drops them (2026-09-27).
|
|
310
|
-
// ⭐ `background` and `theme`
|
|
311
|
-
// them, and where a section stores them
|
|
312
|
-
//
|
|
313
|
-
// stored field
|
|
314
|
+
// ⭐ `background` and `theme` ride IN `params` [Diego, 2026-09-28] — where the runtime reads
|
|
315
|
+
// them, and where a section stores them. ⛔ Not also as the `background` / `theme_override`
|
|
316
|
+
// fields, as build 0.69 sent them: every reader of a stored section reads `params` — a publish
|
|
317
|
+
// carries `params` whole, and the app reads a stored field first, then `params` — so nothing
|
|
318
|
+
// needs them, and a push that stops sending them lets a backend retire them without refusing
|
|
319
|
+
// it. A pull still prefers a stored field while one exists (`sectionRecordToFile`).
|
|
320
|
+
// uwx-format.md § A page section's fields, Order.
|
|
314
321
|
if (Object.keys(params).length > 0) data.params = params
|
|
315
322
|
data.content = section.content // leaf insets in it as written — no `insets` field since 2026-09-27
|
|
316
323
|
setIf(data, 'fetch', section.fetch ?? undefined)
|
|
317
|
-
setIf(data, 'background', params.background)
|
|
318
|
-
setIf(data, 'theme_override', params.theme)
|
|
319
324
|
return data
|
|
320
325
|
}
|
|
321
326
|
|