@uniweb/build 0.52.3 → 0.52.5

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.52.3",
3
+ "version": "0.52.5",
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",
61
- "@uniweb/schemas": "^0.2.15",
62
- "@uniweb/semantic-parser": "^1.4.0",
63
60
  "@uniweb/projections": "^0.7.3",
64
- "@uniweb/theming": "^0.1.15",
65
- "@uniweb/content-writer": "^0.3.4"
61
+ "@uniweb/semantic-parser": "^1.4.1",
62
+ "@uniweb/schemas": "^0.3.1",
63
+ "@uniweb/content-reader": "^1.2.5",
64
+ "@uniweb/content-writer": "^0.3.4",
65
+ "@uniweb/theming": "^0.1.15"
66
66
  },
67
67
  "optionalDependencies": {
68
68
  "@uniweb/runtime": "^0.26.4"
package/src/docs.js CHANGED
@@ -31,6 +31,13 @@ function generateComponentDocs(name, meta) {
31
31
  lines.push('')
32
32
  }
33
33
 
34
+ // The standard section type this component claims. `family` replaced the
35
+ // retired `category:` / `purpose:` pair, which this generator never emitted.
36
+ if (meta.family) {
37
+ lines.push(`*Family:* \`${meta.family}\``)
38
+ lines.push('')
39
+ }
40
+
34
41
  // Parameters (shown first - most important for content authors)
35
42
  if (meta.params && Object.keys(meta.params).length > 0) {
36
43
  lines.push('### Parameters')
@@ -66,33 +73,48 @@ function generateComponentDocs(name, meta) {
66
73
  }
67
74
  }
68
75
 
69
- // Presets
70
- if (meta.presets && meta.presets.length > 0) {
76
+ // Presets — named param combinations. `meta.presets` is a KEYED OBJECT
77
+ // ({ name: { label, params } }), not an array: this read `meta.presets.length`
78
+ // until 2026-09-16, which is `undefined > 0` on an object, so every preset in
79
+ // every foundation was dropped from COMPONENTS.md with nothing reporting it.
80
+ const presets = Object.entries(meta.presets || {})
81
+ if (presets.length > 0) {
71
82
  lines.push('### Presets')
72
83
  lines.push('')
73
84
 
74
- for (const preset of meta.presets) {
75
- const settings = preset.settings
76
- ? Object.entries(preset.settings)
85
+ for (const [name, preset] of presets) {
86
+ const settings = preset?.params
87
+ ? Object.entries(preset.params)
77
88
  .map(([k, v]) => `${k}: ${v}`)
78
89
  .join(', ')
79
90
  : ''
80
- lines.push(`- **${preset.name}**${settings ? ` — ${settings}` : ''}`)
91
+ const label = preset?.label || name
92
+ lines.push(`- **${name}**${label !== name ? ` — ${label}` : ''}${settings ? ` (${settings})` : ''}`)
81
93
  }
82
94
  lines.push('')
83
95
  }
84
96
 
85
- // Content Elements (condensed - less important)
86
- if (meta.elements && Object.keys(meta.elements).length > 0) {
87
- const elements = Object.entries(meta.elements)
88
- const elementList = elements.map(([key, el]) => {
89
- return el.required ? `${key} (required)` : key
90
- }).join(', ')
91
-
97
+ // Content expectations — what an author writes in the markdown. The key is
98
+ // `content`; this read `meta.content` as `meta.elements` until 2026-09-16 —
99
+ // the pre-rename name, which nothing has produced since. A dead branch reads
100
+ // exactly like a component that declares nothing, so the whole section was
101
+ // missing from every generated catalog.
102
+ //
103
+ // A value is either a label string ('Description [1-2]') or { label, hint }.
104
+ // The LABEL is the point: `paragraphs` alone tells an author nothing, and the
105
+ // count hint is the part that says how much to write.
106
+ const elements = Object.entries(meta.content || {})
107
+ if (elements.length > 0) {
92
108
  lines.push('### Content')
93
109
  lines.push('')
94
- lines.push(elementList)
95
- lines.push('')
110
+
111
+ for (const [key, el] of elements) {
112
+ const label = typeof el === 'string' ? el : el?.label || ''
113
+ lines.push(`**${key}**${label ? ` — ${label}` : ''}`)
114
+ const hint = typeof el === 'object' && el?.hint ? el.hint : ''
115
+ if (hint) lines.push(` ${hint}`)
116
+ lines.push('')
117
+ }
96
118
  }
97
119
 
98
120
  return lines.join('\n')
@@ -327,7 +327,8 @@ function visitTranslatableBlocks(nodes, visit) {
327
327
  * Determine field name for heading based on level and index
328
328
  */
329
329
  function getHeadingField(level, index) {
330
- // First H1 is title, first H2 is subtitle, H3 before H1 could be pretitle
330
+ // First H1 is title, first H2 is subtitle. A pretitle is a `#>` label line
331
+ // (or, in older content, a smaller heading above the title).
331
332
  // This is simplified - semantic-parser does this more intelligently
332
333
  if (level === 1) return index.h1 === 0 ? 'title' : `heading.${index.h1}`
333
334
  if (level === 2) return index.h2 === 0 ? 'subtitle' : `heading.h2.${index.h2}`
@@ -23,7 +23,7 @@
23
23
  // Units are DISJOINT: a page's hash excludes its sections, a section's excludes its
24
24
  // children, so a change lands in exactly one unit and is never double-reported.
25
25
  // Naming comes from the projector's own helpers (`pageDirName`,
26
- // `safeStableIdFilename`) rather than a local copy, so a label always names a file
26
+ // `sectionFileBase`) rather than a local copy, so a label always names a file
27
27
  // that really exists.
28
28
  //
29
29
  // It needs TWO bases, and that is the non-obvious part.
@@ -47,7 +47,7 @@
47
47
  // reported as unattributed rather than guessed at.
48
48
 
49
49
  import { entityContentHash } from './records.js'
50
- import { recordStableId, safeStableIdFilename, pageDirName, layoutSectionPaths } from './site-project.js'
50
+ import { sectionFileBase, pageDirName, layoutSectionPaths } from './site-project.js'
51
51
  import { LOCALIZED_FIELD_ASSUMPTION } from './localize.js'
52
52
 
53
53
  // A unit's own content, with the nested collections that are their own units
@@ -95,10 +95,10 @@ export function collectSiteUnits(doc, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.
95
95
  export function walkSiteUnits(doc, cb, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale) {
96
96
  const walkSections = (sections, dir) => {
97
97
  for (const record of sections || []) {
98
- const id = recordStableId(record)
99
- // Anonymous and id-less: the projector cannot place it either, so there is
100
- // no file to name and nothing to attribute.
101
- if (id) cb(`${dir}/${safeStableIdFilename(id)}.md`, record, 'section')
98
+ // The projector's own file rule, so a unit's path here is the file it is
99
+ // written to — including an app-created section named from its `$uuid`.
100
+ const base = sectionFileBase(record)
101
+ if (base) cb(`${dir}/${base}.md`, record, 'section')
102
102
  walkSections(record.$children, dir)
103
103
  }
104
104
  }
@@ -464,6 +464,42 @@ export function safeStableIdFilename(stableId) {
464
464
  return base ? `${base}-${hash}` : `s-${hash}`
465
465
  }
466
466
 
467
+ /**
468
+ * The `.md` file base a page section is written to — the ONE rule the projector,
469
+ * the prune keep-set and the push/pull unit walk (`site-diff.js::walkSiteUnits`)
470
+ * all use, so the path a pull harvests a `$uuid` under is the path the next push
471
+ * stamps it back onto.
472
+ *
473
+ * `stable_id || $id` when the record has one (unchanged). ⛔ A section with
474
+ * neither — what the app's editor creates: a `$uuid` and no stable id — used to be
475
+ * SKIPPED, silently, by all three. A pull then left its file out, and a push after
476
+ * any local edit sent the page without it, which the backend deletes (a
477
+ * package's absence of a stored `page_sections` item under a page it sends is a
478
+ * delete). So such a section is named from its identity instead:
479
+ * `<type-kebab>-<last 8 hex of $uuid>`, e.g. `hero-95993360`.
480
+ *
481
+ * The derived base is already filesystem-safe and starts with a letter, so the
482
+ * build reads it back verbatim as the section's stable id (no numeric prefix to
483
+ * strip) and `safeStableIdFilename` returns it unchanged — the push path equals
484
+ * the pull path. The first push after such a pull therefore gives the item that
485
+ * stable id; the `$uuid` is re-stamped by path, so the item keeps its identity.
486
+ *
487
+ * @returns {string|null} null only for a record with no stable id and no `$uuid`
488
+ */
489
+ export function sectionFileBase(record) {
490
+ const id = recordStableId(record)
491
+ if (id) return safeStableIdFilename(id)
492
+ const hex = typeof record?.$uuid === 'string' ? record.$uuid.replace(/-/g, '').toLowerCase() : ''
493
+ if (!/^[0-9a-f]{8,}$/.test(hex)) return null
494
+ const kebab = String(record?.type || '')
495
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
496
+ .toLowerCase()
497
+ .replace(/[^a-z0-9]+/g, '-')
498
+ .replace(/^-+|-+$/g, '')
499
+ const stem = /^[a-z]/.test(kebab) ? kebab : 'section'
500
+ return `${stem}-${hex.slice(-8)}`
501
+ }
502
+
467
503
  /**
468
504
  * Write a page's `page_sections` tree to clean `<stableId>.md` files in `pageDir`
469
505
  * and return the `page.yml::sections:` array that captures order + nesting — the
@@ -483,12 +519,13 @@ export function pageSectionsToFiles({ pageDir, pageSections, ctx, pageContext })
483
519
  const buildEntries = (records) => {
484
520
  const entries = []
485
521
  for (const record of records || []) {
486
- const stableId = recordStableId(record)
487
- if (!stableId) continue // anonymous and id-less → cannot place; skip
488
- // Filesystem-safe filename (= stableId when already safe); the true stableId
489
- // rides in frontmatter `id:`. The sections: leaf uses the same base so the
490
- // producer's filename-based resolution matches.
491
- const fileBase = safeStableIdFilename(stableId)
522
+ // Filesystem-safe filename (= stableId when already safe; derived from `$uuid`
523
+ // for an app-created section with no stable id); the true stableId rides in
524
+ // frontmatter `id:`. The sections: leaf uses the same base so the producer's
525
+ // filename-based resolution matches.
526
+ const fileBase = sectionFileBase(record)
527
+ if (!fileBase) continue // no stable id AND no `$uuid` → nothing to name it by
528
+ const stableId = recordStableId(record) || fileBase
492
529
  const filePath = join(pageDir, `${fileBase}.md`)
493
530
  // If this uuid's section moved (an app-side stableId rename), relocate its
494
531
  // `.md` in place before writing; then record its current path in the index.
@@ -672,8 +709,8 @@ function collectSectionFileBases(pageSections) {
672
709
  const bases = new Set()
673
710
  const walk = (records) => {
674
711
  for (const record of records || []) {
675
- const id = recordStableId(record)
676
- if (id) bases.add(safeStableIdFilename(id))
712
+ const base = sectionFileBase(record)
713
+ if (base) bases.add(base)
677
714
  if (Array.isArray(record.$children)) walk(record.$children)
678
715
  }
679
716
  }