@uniweb/build 0.67.1 → 0.69.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.67.1",
3
+ "version": "0.69.0",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,12 +57,12 @@
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.5",
60
+ "@uniweb/schemas": "^0.8.3",
61
61
  "@uniweb/content-writer": "^0.3.4",
62
- "@uniweb/schemas": "^0.8.1",
63
- "@uniweb/semantic-parser": "^1.4.1",
64
- "@uniweb/projections": "^0.7.7",
65
- "@uniweb/theming": "^0.1.15"
62
+ "@uniweb/theming": "^0.1.15",
63
+ "@uniweb/content-reader": "^1.2.7",
64
+ "@uniweb/projections": "^0.7.8",
65
+ "@uniweb/semantic-parser": "^1.4.1"
66
66
  },
67
67
  "optionalDependencies": {
68
68
  "@uniweb/runtime": "^0.27.10"
package/src/schema.js CHANGED
@@ -17,6 +17,7 @@ import { isFontVar } from '@uniweb/theming'
17
17
  import { join, dirname, extname, basename } from 'node:path'
18
18
  import { pathToFileURL } from 'node:url'
19
19
  import { describeChildren, describeVisuals } from '@uniweb/schemas/component'
20
+ import { SECTION_PARAMS } from '@uniweb/schemas/section'
20
21
  import { collectSchemaRefs, buildDataSchemaMap, ownSchemaRefs } from './resolve-data-schema.js'
21
22
  import {
22
23
  composeSupports,
@@ -957,13 +958,17 @@ export function reportPlacementDeclarations(components) {
957
958
  `provides renders "Component not found".`
958
959
  )
959
960
  }
960
- // `grid` is a section key framework reserves for the layout of a section's children,
961
- // so a param by that name never receives what an author writes as `grid:`.
962
- if (entry.params && typeof entry.params === 'object' && Object.hasOwn(entry.params, 'grid')) {
961
+ // A name framework reserves in a section's params whose value the component never
962
+ // receives — `component: false` in `@uniweb/schemas/section`, as `grid` is.
963
+ const params = entry.params && typeof entry.params === 'object' ? entry.params : {}
964
+ for (const [param, reserved] of Object.entries(SECTION_PARAMS)) {
965
+ if (reserved.component || !Object.hasOwn(params, param)) continue
963
966
  console.warn(
964
- `Warning: ${name} (meta.js): a param named "grid" never receives the author's grid: — it is a section ` +
965
- `key reserved for the layout of child sections. Offer layouts with children: { grid: [...] } and ` +
966
- `lay the children out with kit's ChildGrid, or rename the param.`
967
+ `Warning: ${name} (meta.js): a param named "${param}" never receives the author's ${param}: — framework ` +
968
+ `reserves it in a section's params (@uniweb/schemas/section). Rename the param.` +
969
+ (param === 'grid'
970
+ ? ` To offer layouts for the child sections, declare children: { grid: [...] } and lay them out with kit's ChildGrid.`
971
+ : '')
967
972
  )
968
973
  }
969
974
  }
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * Section frontmatter reserved properties:
12
12
  * - type: Component type (e.g., "Hero", "Features")
13
- * - preset: Preset configuration name
14
- * - input: Input field mapping
13
+ * - hidden: `true` makes the section a draft — left out of a published build, kept in dev
15
14
  * - props: Additional component props (merged with other params)
16
15
  * - fetch: The section's binding — names a query (query, as, where, sort, limit, current,
17
16
  * prerender, detailPage); `query:` is its shorthand
@@ -33,7 +32,7 @@ import yaml from 'js-yaml'
33
32
  import { YAML_OPTIONS } from '../utils/yaml-schema.js'
34
33
  import { collectSectionAssets, mergeAssetCollections, collectConfigAssets } from './assets.js'
35
34
  import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
36
- import { normalizeHideIn, dropUnpublishedPages } from './nav-visibility.js'
35
+ import { normalizeHideIn, dropUnpublishedPages, dropHiddenSections } from './nav-visibility.js'
37
36
  import { parseFetchConfig, toFetchList } from './data-fetcher.js'
38
37
  import { resolveExtensionUrls } from './extension-urls.js'
39
38
  import { buildTheme, extractFoundationVars } from '../theme/index.js'
@@ -1011,7 +1010,28 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
1011
1010
 
1012
1011
  // `query`, `fetch` and `data` are never params: `query:` / `fetch:` declare the
1013
1012
  // section's own data, and a leftover `data:` is refused (`declaredFetch`).
1014
- const { type, preset, input, props, fetch, query, data, id: frontmatterId, ...params } = frontMatter
1013
+ const { type, hidden, preset, input, props, fetch, query, data, id: frontmatterId, ...params } = frontMatter
1014
+ // ⭐ `hidden: true` — a draft section, the page's meaning one level down: left out of a
1015
+ // published build, kept by `uniweb dev`, and synced as the section's own `hidden`
1016
+ // [Diego, 2026-09-28]. ⛔ Until then it was an ordinary param: the component received it
1017
+ // and the section rendered everywhere.
1018
+ if (hidden !== undefined && typeof hidden !== 'boolean') {
1019
+ console.warn(
1020
+ `[content-collector] ${relative(siteRoot, filePath)}: hidden: ${JSON.stringify(hidden)} is not true or false — ` +
1021
+ `the section stays visible.`
1022
+ )
1023
+ }
1024
+ // ⛔ `preset:` and `input:` do nothing, so they are dropped with a warning rather than
1025
+ // handed to the component as params. Nothing ever read either [2026-09-27]: a preset is
1026
+ // a named set of params a section type's `meta.js` offers an editor, and a file writes
1027
+ // the params themselves (`uniweb add section <Type> --starter --preset <name>` writes them).
1028
+ for (const [key, value] of [['preset', preset], ['input', input]]) {
1029
+ if (value === undefined) continue
1030
+ console.warn(
1031
+ `[content-collector] ${relative(siteRoot, filePath)}: ${key}: ${JSON.stringify(value)} has no effect and is ignored` +
1032
+ (key === 'preset' ? ' — write the preset’s params in the frontmatter instead.' : '.')
1033
+ )
1034
+ }
1015
1035
  // ⭐ `grid` — the layout of the section's child sections, `3` or `'40/60'` [Diego,
1016
1036
  // 2026-09-27] — is a name framework reserves, and it stays in the params like
1017
1037
  // `background`: that is where it is stored and synced, with no field of its own.
@@ -1049,8 +1069,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
1049
1069
  id,
1050
1070
  stableId,
1051
1071
  type: type || null,
1052
- preset,
1053
- input,
1072
+ ...(hidden === true ? { hidden: true } : {}),
1054
1073
  params: { ...params, ...props },
1055
1074
  content: proseMirrorContent,
1056
1075
  fetch: parseFetchConfig(resolvedFetch, relative(siteRoot, filePath), { level: 'section' }),
@@ -2783,10 +2802,11 @@ export async function collectSiteContent(sitePath, options = {}) {
2783
2802
  links: themeLinks
2784
2803
  },
2785
2804
  // Reachability axis: on the published build paths, drop `hidden` pages and
2786
- // their whole subtree (cascade). Dev keeps them so drafts stay previewable.
2787
- pages: dropUnpublished ? dropUnpublishedPages(pages) : pages,
2805
+ // their whole subtree (cascade), and every `hidden` section with its children.
2806
+ // Dev keeps them so drafts stay previewable.
2807
+ pages: dropUnpublished ? dropUnpublishedPages(pages).map(withoutHiddenSections) : pages,
2788
2808
  // Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
2789
- layouts,
2809
+ layouts: dropUnpublished ? mapLayoutAreas(layouts, withoutHiddenSections) : layouts,
2790
2810
  // ⭐ THE SAME REACHABILITY AXIS, for the 404 slot. `hidden: true` means DRAFT —
2791
2811
  // "excluded from the published site" (`docs/reference/page-configuration.md`);
2792
2812
  // `hideIn: ['*']` is the control for "routed but in no nav".
@@ -2799,7 +2819,7 @@ export async function collectSiteContent(sitePath, options = {}) {
2799
2819
  // shell), and `uniweb dev` keeps the page previewable like any other draft.
2800
2820
  // No cascade to resolve: this slot is root-level only, so its own flag is
2801
2821
  // the only one that can apply.
2802
- notFound: dropUnpublished && notFound?.hidden ? null : notFound,
2822
+ notFound: dropUnpublished ? (notFound?.hidden ? null : withoutHiddenSections(notFound)) : notFound,
2803
2823
  // Versioned scopes: route → { versions, latestId }
2804
2824
  versionedScopes: versionedScopesObj,
2805
2825
  assets: assetCollection.assets,
@@ -2810,6 +2830,25 @@ export async function collectSiteContent(sitePath, options = {}) {
2810
2830
  }
2811
2831
  }
2812
2832
 
2833
+ // A page (or a layout area) without its hidden sections — `dropHiddenSections`.
2834
+ function withoutHiddenSections(page) {
2835
+ if (!page || !Array.isArray(page.sections)) return page
2836
+ return { ...page, sections: dropHiddenSections(page.sections) }
2837
+ }
2838
+
2839
+ // Apply `fn` to every area of every layout: `{ default: { header: page, … }, … }`.
2840
+ function mapLayoutAreas(layouts, fn) {
2841
+ if (!layouts || typeof layouts !== 'object') return layouts
2842
+ return Object.fromEntries(
2843
+ Object.entries(layouts).map(([name, areas]) => [
2844
+ name,
2845
+ areas && typeof areas === 'object'
2846
+ ? Object.fromEntries(Object.entries(areas).map(([area, page]) => [area, fn(page)]))
2847
+ : areas,
2848
+ ])
2849
+ )
2850
+ }
2851
+
2813
2852
  // Shared pure / IO helpers. Exported for testing AND reused by
2814
2853
  // @uniweb/build/uwx, the .uwx exporter, which re-walks the source rather
2815
2854
  // than the flattened collector output but reuses these primitives so
@@ -1,6 +1,7 @@
1
1
  // Page-visibility helpers — the two orthogonal axes a page carries:
2
2
  // • Reachability — `hidden: bool`. `true` = excluded from the PUBLISHED output
3
- // entirely (see `dropUnpublishedPages`). Still previewable in `uniweb dev`.
3
+ // entirely (see `dropUnpublishedPages`, and `dropHiddenSections` for a section).
4
+ // Still previewable in `uniweb dev`.
4
5
  // • Nav placement — `hideIn: string[]`. Which nav areas suppress the page while
5
6
  // it IS routed (see `normalizeHideIn`). `['*']` = suppressed from every area.
6
7
 
@@ -67,3 +68,23 @@ export function dropUnpublishedPages(pages) {
67
68
  }
68
69
  return pages.filter((p) => !isUnpublished(p))
69
70
  }
71
+
72
+ /**
73
+ * Drop hidden sections — the same reachability axis, one level down. A section with
74
+ * `hidden: true` in its frontmatter is a draft: left out of the PUBLISHED output together
75
+ * with its child sections, and kept by `uniweb dev` so it stays previewable. Applied where
76
+ * `dropUnpublishedPages` is, to a page's sections and a layout area's alike.
77
+ *
78
+ * @param {Array<object>} sections - collected sections (each may hold `subsections`)
79
+ * @returns {Array<object>} the sections without the hidden ones, at every depth
80
+ */
81
+ export function dropHiddenSections(sections) {
82
+ if (!Array.isArray(sections)) return sections
83
+ return sections
84
+ .filter((section) => section?.hidden !== true)
85
+ .map((section) =>
86
+ Array.isArray(section.subsections) && section.subsections.length > 0
87
+ ? { ...section, subsections: dropHiddenSections(section.subsections) }
88
+ : section
89
+ )
90
+ }
@@ -41,7 +41,8 @@ export const DEFAULT_RESERVED_FRONTMATTER = new Set([
41
41
  'fetch',
42
42
  'data',
43
43
  'nest',
44
- 'hidden',
44
+ // ⛔ Not `hidden`, which was here from the start (2026-05-30) with no reason recorded: an
45
+ // editor toggles it, so a pull must carry the store's value into the file (2026-09-28).
45
46
  ])
46
47
 
47
48
  // js-yaml dump options shared by every config write, so output is byte-stable.
@@ -425,7 +425,7 @@ function reinlineInsets(content, insets) {
425
425
  * Project one section `$`-record (from `page_sections` / `layout_sections`) to a
426
426
  * section `.md` file — the inverse of site.js `mapSectionData`. Frontmatter is
427
427
  * `type` + the flat `params` + `background` / `theme` (`theme_override`) /
428
- * `preset` / `input` / `fetch` / `id` (`stable_id`); the body is the section's
428
+ * `fetch` / `id` (`stable_id`); the body is the section's
429
429
  * content (insets re-inlined) serialized to markdown. Idempotent.
430
430
  *
431
431
  * Note: `$children` (a section's child sections) are NOT written here — the page
@@ -442,7 +442,9 @@ function reinlineInsets(content, insets) {
442
442
  * @returns {'updated'|'unchanged'}
443
443
  */
444
444
  export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, freeformRelPath, freeformCandidates = null, writeId = true, context = null }) {
445
- const { type, stable_id, preset, input, params, content, insets, fetch, background, theme_override } = record || {}
445
+ // ⛔ A stored `preset` or `input` is not written back: neither has an effect, and the build
446
+ // would only warn about it (2026-09-27).
447
+ const { type, stable_id, hidden, params, content, insets, fetch, background, theme_override } = record || {}
446
448
 
447
449
  // A localized `content` field unwraps to the source-locale doc for the body; its
448
450
  // target-locale structural maps are captured into the locales/ collector, and any
@@ -450,13 +452,20 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
450
452
  // locales/freeform/. A bare doc (source-only / pre-localization) passes through.
451
453
  const sourceContent = unwrapLocalizedContent(content, sourceLocale, collector, freeformRelPath, freeformCandidates, context)
452
454
 
455
+ const { hidden: paramHidden, ...paramsRest } = params && typeof params === 'object' ? params : {}
453
456
  const frontmatter = {}
454
457
  if (type !== undefined) frontmatter.type = type
455
- if (params && typeof params === 'object') Object.assign(frontmatter, params)
458
+ // ⭐ `hidden:` says what the store says: written for a hidden section, and removed from the
459
+ // file of a visible one — an editor toggles it [2026-09-28]. ⛔ Until then a pull dropped it,
460
+ // and the next push un-hid the section. A push before that day sent a file's `hidden:` inside
461
+ // `params`, so one stored there counts when the field is absent.
462
+ frontmatter.hidden = (typeof hidden === 'boolean' ? hidden : paramHidden === true) ? true : undefined
463
+ 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).
456
467
  if (background !== undefined) frontmatter.background = background
457
468
  if (theme_override !== undefined) frontmatter.theme = theme_override
458
- if (preset !== undefined) frontmatter.preset = preset
459
- if (input !== undefined) frontmatter.input = input
460
469
  // Invert the build's resolution rather than copy it — see fetch-shapes.js. A new
461
470
  // file gets `query:` for a declaration of names alone; a section that declares
462
471
  // its data locally keeps it (`writeSectionFile`, the declaration keys).
package/src/uwx/site.js CHANGED
@@ -142,9 +142,9 @@ function stripCredentials(block, label) {
142
142
  }
143
143
 
144
144
  // page_sections and layout_sections share this content shape.
145
- // processMarkdownFile only destructures type/component/preset/input/props/
146
- // fetch/data/id out of frontmatter, so `background:` and `theme:` stay
147
- // inside section.params — lift them into the entity type's dedicated fields.
145
+ // processMarkdownFile takes only type/hidden/props/fetch/query/data/id out of the
146
+ // frontmatter (and drops preset/input), so `background:` and `theme:` stay
147
+ // inside section.params — and ride there on the wire too (`mapSectionData`).
148
148
  // The `type` a push sends for a section whose file names none — the Model requires one. A pull does
149
149
  // not write it back into a file that names none (`sectionRecordToFile`): absent, the foundation's
150
150
  // `defaultSection` renders it, which is not always `Content`.
@@ -302,20 +302,20 @@ function localizePageMeta(page, route, sourceLocale, translations) {
302
302
 
303
303
  function mapSectionData(section) {
304
304
  const params = { ...section.params }
305
- const background = params.background
306
- const themeOverride = params.theme
307
- delete params.background
308
- delete params.theme
309
-
310
305
  const data = { type: section.type || FILLED_SECTION_TYPE } // entity type requires `type`
311
306
  setIf(data, 'stable_id', section.stableId ?? undefined)
312
- setIf(data, 'preset', section.preset ?? undefined)
313
- setIf(data, 'input', section.input ?? undefined)
307
+ // ⭐ A draft section — the store's own `hidden`, which an editor also writes [2026-09-28].
308
+ if (section.hidden === true) data.hidden = true
309
+ // No `preset` or `input`: nothing read either, and the collector drops them (2026-09-27).
310
+ // ⭐ `background` and `theme` stay IN `params` [Diego, 2026-09-28] — where the runtime reads
311
+ // them, and where a section stores them from now on. ⚠️ Also sent as the two fields they had
312
+ // until today, until every reader of a stored section reads `params`; a pull prefers a
313
+ // stored field meanwhile (`sectionRecordToFile`). uwx-format.md § A page section's fields.
314
314
  if (Object.keys(params).length > 0) data.params = params
315
315
  data.content = section.content // leaf insets in it as written — no `insets` field since 2026-09-27
316
316
  setIf(data, 'fetch', section.fetch ?? undefined)
317
- setIf(data, 'background', background)
318
- setIf(data, 'theme_override', themeOverride)
317
+ setIf(data, 'background', params.background)
318
+ setIf(data, 'theme_override', params.theme)
319
319
  return data
320
320
  }
321
321