@uniweb/build 0.11.5 → 0.11.7

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.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -58,8 +58,8 @@
58
58
  "@uniweb/theming": "0.1.3"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@uniweb/runtime": "0.8.7",
62
- "@uniweb/content-reader": "1.1.7",
61
+ "@uniweb/runtime": "0.8.9",
62
+ "@uniweb/content-reader": "1.1.8",
63
63
  "@uniweb/schemas": "0.2.1"
64
64
  },
65
65
  "peerDependencies": {
@@ -69,7 +69,7 @@
69
69
  "@tailwindcss/vite": "^4.0.0",
70
70
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
71
71
  "vite-plugin-svgr": "^4.0.0",
72
- "@uniweb/core": "0.7.6"
72
+ "@uniweb/core": "0.7.8"
73
73
  },
74
74
  "peerDependenciesMeta": {
75
75
  "vite": {
@@ -31,8 +31,9 @@ export {
31
31
  singularize,
32
32
  } from '../site/data-fetcher.js'
33
33
 
34
- // Cross-reference registry. Walks the document tree, finds every
35
- // block-level element with a {#id} attribute, registers the id with
36
- // its inferred kind + counter. Consumed by the framework's <Ref>
37
- // component to render `[#id]` cross-references.
38
- export { buildXrefRegistry } from '../site/xref-registry.js'
34
+ // Cross-reference registry moved out of build into kit
35
+ // (@uniweb/kit/xref). Foundations that need cross-references import
36
+ // `buildXrefRegistry` from there and re-export it via
37
+ // `foundation.xref.build`; the runtime calls it during initialization
38
+ // (setup.js / ssr-renderer.js). See framework/kit/src/xref/registry.js
39
+ // for the implementation.
package/src/prerender.js CHANGED
@@ -11,7 +11,6 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'
11
11
  import { existsSync, readdirSync, statSync } from 'node:fs'
12
12
  import { join, dirname, resolve } from 'node:path'
13
13
  import { pathToFileURL } from 'node:url'
14
- import { deriveCacheKey } from '@uniweb/core'
15
14
  import { executeFetch, mergeDataIntoContent, singularize } from './site/data-fetcher.js'
16
15
  import { shouldSplitContent } from './site/split-content.js'
17
16
 
@@ -420,6 +419,7 @@ export async function prerenderSite(siteDir, options = {}) {
420
419
  // Load shared SSR functions from runtime (lazy — only when prerendering)
421
420
  const {
422
421
  initPrerender,
422
+ hydrateDataStore,
423
423
  prefetchIcons,
424
424
  renderPage,
425
425
  injectPageContent,
@@ -557,12 +557,10 @@ export async function prerenderSite(siteDir, options = {}) {
557
557
  const uniweb = initPrerender(siteContent, foundation, loadedExtensions, { onProgress })
558
558
 
559
559
  // Build-specific: pre-populate DataStore so EntityStore can resolve data during prerender.
560
- // Use the framework's default cache key so runtime probes hit the same entries.
561
- if (fetchedData.length > 0 && uniweb.activeWebsite?.dataStore) {
562
- for (const entry of fetchedData) {
563
- uniweb.activeWebsite.dataStore.set(deriveCacheKey(entry.config), { data: entry.data })
564
- }
565
- }
560
+ // hydrateDataStore handles cache-key derivation + value-shape wrapping
561
+ // same helper used by the browser SPA boot and by the Cloudflare
562
+ // Worker SSR isolate, so all three render paths agree on shape.
563
+ hydrateDataStore(uniweb.activeWebsite, fetchedData)
566
564
 
567
565
  // Pre-fetch icons for SSR embedding
568
566
  await prefetchIcons(siteContent, uniweb, onProgress)
@@ -42,13 +42,30 @@ async function buildSchemaWithPreviews(srcDir, outDir, isProduction, sectionPath
42
42
  let _buildingSSRBundle = false
43
43
 
44
44
  /**
45
- * Build a self-contained ESM bundle for edge SSR (Dynamic Workers).
45
+ * Build a self-contained ESM bundle for edge SSR (Cloudflare Dynamic Workers).
46
46
  *
47
- * Produces foundation.ssr.js — a single ESM file with React, ReactDOM/server,
48
- * @uniweb/core, and the foundation components all inlined. No external imports.
47
+ * Produces `ssr-worker-bundle.js` — a single ESM file with React,
48
+ * ReactDOM/server, `@uniweb/core`, `@uniweb/runtime/ssr`,
49
+ * `@uniweb/theming`, and the foundation's components all inlined. No
50
+ * external imports.
49
51
  *
50
- * This bundle is loaded into a Cloudflare Dynamic Worker isolate at request time
51
- * via env.LOADER.get(). The isolate caches the bundle per foundation version.
52
+ * This is **NOT a foundation**. The foundation is `dist/foundation.js`
53
+ * a foundation-shaped ESM module that externalizes runtime and links
54
+ * to it at runtime, the same as in browser SPA / framework SSG / unipress
55
+ * (Node SSR). This file is something different: a self-contained SSR
56
+ * pipeline shaped for the Cloudflare Workers Dynamic Worker LOADER, with
57
+ * the foundation embedded as one of several inputs. The size ratio
58
+ * (typical ~12× larger than `foundation.js`) reflects what's actually
59
+ * inside it.
60
+ *
61
+ * The artifact is bundled this way because the Dynamic Worker LOADER
62
+ * accepts a closed `modules` map at isolate construction (no external
63
+ * resolver, no path back to npm or R2 for transitive imports). One
64
+ * file in, everything resolves. A future refactor can switch to side-
65
+ * loading runtime + React + core into the modules map separately so
66
+ * foundations stop carrying runtime in their bundles — see
67
+ * `kb/platform/plans/edge-runtime-side-loading.md`. Until then, this
68
+ * build pre-bundles for the isolate's contract.
52
69
  *
53
70
  * @param {string} outDir - Path to dist/ directory containing foundation.js
54
71
  */
@@ -142,7 +159,7 @@ async function buildSSRBundle(outDir) {
142
159
  bundle: true,
143
160
  format: 'esm',
144
161
  platform: 'browser',
145
- outfile: join(outDir, 'foundation.ssr.js'),
162
+ outfile: join(outDir, 'ssr-worker-bundle.js'),
146
163
  minify: false,
147
164
  external: [],
148
165
  nodePaths,
@@ -151,9 +168,9 @@ async function buildSSRBundle(outDir) {
151
168
  logLevel: 'warning',
152
169
  })
153
170
 
154
- const ssrFile = join(outDir, 'foundation.ssr.js')
171
+ const ssrFile = join(outDir, 'ssr-worker-bundle.js')
155
172
  const size = (statSync(ssrFile).size / 1024).toFixed(1)
156
- console.log(`Generated foundation.ssr.js (${size} KB)`)
173
+ console.log(`Generated ssr-worker-bundle.js (${size} KB)`)
157
174
  } catch (err) {
158
175
  console.warn(`Warning: SSR bundle build failed: ${err.message}`)
159
176
  } finally {
@@ -1,192 +0,0 @@
1
- /**
2
- * Cross-reference registry — populated at content-collection time.
3
- *
4
- * Walks the parsed document tree to find every block-level element
5
- * carrying a {#id} attribute, infers the element's kind from its node
6
- * type, and records the entry with a per-kind counter. The result
7
- * attaches to siteContent.xref and is consumed by the framework's
8
- * <Ref> component to render `[#id]` cross-references.
9
- *
10
- * Built-in kinds:
11
- * - heading → 'section' (hierarchical counter: 1, 1.1, 1.1.1, 2, …)
12
- * - image → 'figure' (flat arabic counter)
13
- * - math_display → 'equation' (flat arabic counter)
14
- * - table → 'table' (flat arabic counter)
15
- *
16
- * Foundation extensions: foundations may declare additional kinds via
17
- * `foundation.xref.kinds`. The id-collection pass uses the foundation's
18
- * declared `prefix` (e.g. `thm` → `theorem`) to classify ids that don't
19
- * land on a built-in element type.
20
- *
21
- * The registry is per-document, not per-page: counters span the whole
22
- * document so authors get consistent numbering even when their document
23
- * spans multiple pages. (For page-local counter resets, foundations can
24
- * declare `resetOn: 'chapter'` on a kind — applied during render, not
25
- * here.)
26
- *
27
- * Output shape:
28
- * {
29
- * entries: {
30
- * <id>: {
31
- * id, // the id itself
32
- * kind, // 'figure' | 'equation' | 'section' | 'table' | <foundation-declared>
33
- * counter, // number for flat kinds, dotted string for hierarchical
34
- * counterText, // displayable counter ('3', '3.2')
35
- * sourcePath, // page route the id was declared on (for back-refs)
36
- * caption, // (figures, tables) caption attr, when set
37
- * text, // (sections) heading's plain text content
38
- * latex, // (equations) latex source for the equation
39
- * },
40
- * },
41
- * }
42
- *
43
- * `caption`, `text`, and `latex` are populated only for the elements
44
- * that carry them — they're undefined on other kinds. List sections
45
- * (ListOfFigures / ListOfTables / TableOfContents) read these to render
46
- * "Figure 3 — A diagram of mitosis ........ 47" entries without
47
- * re-walking the document tree. Other consumers (the framework's <Ref>
48
- * component, Typst / LaTeX cross-reference emitters) ignore them.
49
- */
50
-
51
- const KIND_BY_TYPE = {
52
- heading: 'section',
53
- image: 'figure',
54
- math_display: 'equation',
55
- table: 'table',
56
- }
57
-
58
- export function buildXrefRegistry(siteContent, options = {}) {
59
- const { foundationKinds = {}, onWarn = (msg) => console.warn(msg) } = options
60
-
61
- const entries = {}
62
- const flatCounters = {} // kind -> next integer counter (figure, equation, table, foundation-declared kinds)
63
- const sectionStack = [] // hierarchical counters per heading level
64
-
65
- // Index foundation prefixes for id-prefix inference.
66
- const prefixToKind = {}
67
- for (const [kind, meta] of Object.entries(foundationKinds)) {
68
- if (meta?.prefix) prefixToKind[meta.prefix] = kind
69
- }
70
-
71
- function nextFlat(kind) {
72
- flatCounters[kind] = (flatCounters[kind] || 0) + 1
73
- return flatCounters[kind]
74
- }
75
-
76
- // Track the shallowest heading depth seen — chapter content commonly
77
- // starts at h2 (chapter title is rendered separately as h1 by the
78
- // foundation), so the first encountered level becomes the document's
79
- // top counter level. Re-anchoring: if a later heading appears at a
80
- // shallower level than `topLevel`, drop topLevel down to match.
81
- let topLevel = null
82
-
83
- function nextHierarchical(level) {
84
- if (topLevel == null || level < topLevel) topLevel = level
85
- const depth = level - topLevel + 1 // 1-based depth from the document's top level
86
-
87
- // Trim deeper levels when surfacing back to a shallower heading.
88
- while (sectionStack.length >= depth) sectionStack.pop()
89
- while (sectionStack.length < depth - 1) sectionStack.push(1) // implicit parents start at 1
90
- sectionStack.push((sectionStack[depth - 1] || 0) + 1)
91
- sectionStack.length = depth
92
- return sectionStack.slice().join('.')
93
- }
94
-
95
- // Collect the plain text content of a node tree — used to capture a
96
- // heading's displayable text for ListOfSections-style entries. Walks
97
- // the standard ProseMirror text shape (text nodes carry `.text`;
98
- // structural nodes recurse via `.content`).
99
- function collectTextContent(node) {
100
- if (!node || typeof node !== 'object') return ''
101
- if (node.type === 'text' && typeof node.text === 'string') return node.text
102
- if (Array.isArray(node.content)) {
103
- return node.content.map(collectTextContent).join('')
104
- }
105
- return ''
106
- }
107
-
108
- function inferKind(node) {
109
- // 1. Node-type-based: built-ins.
110
- const builtin = KIND_BY_TYPE[node.type]
111
- if (builtin) return builtin
112
- // 2. Explicit kind attribute on the node ({.kind} or kind=…).
113
- if (node.attrs?.kind && (KIND_BY_TYPE[node.attrs.kind] || foundationKinds[node.attrs.kind])) {
114
- return node.attrs.kind
115
- }
116
- // 3. Foundation-declared prefix on the id.
117
- const id = node.attrs?.id
118
- if (id && id.includes('-')) {
119
- const prefix = id.slice(0, id.indexOf('-'))
120
- if (prefixToKind[prefix]) return prefixToKind[prefix]
121
- }
122
- return null
123
- }
124
-
125
- function visit(node, sourcePath) {
126
- if (!node || typeof node !== 'object') return
127
-
128
- // Headings get a hierarchical counter regardless of whether they
129
- // carry an id — the {#id} just labels them; the counter itself is
130
- // assigned by tree position.
131
- let counter = null
132
- let counterText = null
133
- if (node.type === 'heading') {
134
- const level = Math.max(1, Math.min(6, node.attrs?.level || 1))
135
- counterText = nextHierarchical(level)
136
- counter = counterText
137
- }
138
-
139
- const id = node.attrs?.id
140
- if (id) {
141
- const kind = inferKind(node)
142
- if (!kind) {
143
- onWarn(`[xref] {#${id}} on unrecognized element type "${node.type}" — ignored`)
144
- } else if (entries[id]) {
145
- onWarn(`[xref] duplicate id "${id}" — keeping first registration`)
146
- } else {
147
- if (counter == null) {
148
- counter = nextFlat(kind)
149
- counterText = String(counter)
150
- }
151
- const entry = { id, kind, counter, counterText, sourcePath: sourcePath || '' }
152
- // Per-kind metadata. List sections (ListOfFigures, ListOfTables,
153
- // TableOfContents) read these directly so they don't have to
154
- // re-walk the parsed tree to find captions and headings.
155
- const captionAttr = node.attrs?.caption
156
- if (kind === 'figure' || kind === 'table') {
157
- if (captionAttr) entry.caption = String(captionAttr)
158
- }
159
- if (node.type === 'heading') {
160
- const text = collectTextContent(node)
161
- if (text) entry.text = text
162
- }
163
- if (node.type === 'math_display') {
164
- const latex = node.attrs?.latex
165
- if (latex) entry.latex = String(latex)
166
- }
167
- entries[id] = entry
168
- }
169
- } else if (node.type === 'heading') {
170
- // Heading without an id still advances the counter — but we don't
171
- // register it (no way to reference it). The advance is needed so
172
- // that the *next* labeled heading gets the right hierarchical
173
- // counter.
174
- // counter already computed above; just no entry.
175
- }
176
-
177
- if (Array.isArray(node.content)) {
178
- for (const child of node.content) visit(child, sourcePath)
179
- }
180
- }
181
-
182
- for (const page of siteContent.pages || []) {
183
- for (const section of page.sections || []) {
184
- const content = section.content
185
- if (content?.type === 'doc' && Array.isArray(content.content)) {
186
- for (const child of content.content) visit(child, page.route)
187
- }
188
- }
189
- }
190
-
191
- return { entries }
192
- }