@uniweb/runtime 0.8.40 → 0.8.42

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/runtime",
3
- "version": "0.8.40",
3
+ "version": "0.8.42",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,15 +35,15 @@
35
35
  "node": ">=20.19"
36
36
  },
37
37
  "dependencies": {
38
- "@uniweb/core": "0.7.33",
39
- "@uniweb/theming": "0.1.14"
38
+ "@uniweb/core": "0.7.34",
39
+ "@uniweb/theming": "0.1.15"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@vitejs/plugin-react": "^4.5.2",
43
43
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
44
44
  "vite": "^7.3.1",
45
45
  "vitest": "^4.1.7",
46
- "@uniweb/build": "0.15.10"
46
+ "@uniweb/build": "0.15.13"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^19.0.0",
package/src/setup.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  } from 'react-router-dom'
29
29
 
30
30
  import { ChildBlocks } from './components/PageRenderer.jsx'
31
- import { wireFoundationCapabilities, hydrateDataStore } from './wire-foundation.js'
31
+ import { wireFoundationCapabilities, hydrateDataStore, ensureThemeCss } from './wire-foundation.js'
32
32
 
33
33
  // Re-export so existing consumers importing hydrateDataStore from
34
34
  // setup.js keep working. The implementation moved to wire-foundation.js
@@ -274,6 +274,18 @@ export function initUniweb({ content, foundation, extensions = [], routingCompon
274
274
  // here vs. ssr-renderer.js.
275
275
  wireFoundationCapabilities(uniweb, foundation)
276
276
 
277
+ // Site-wide theme CSS, for lanes that arrive without it (a backend-hosted
278
+ // SPA carries the authored theme.yml, not generated CSS). Skipped when a
279
+ // prerendered page already has the style tag in <head>: the build strips
280
+ // `theme.css` from __SITE_CONTENT__ precisely because it is already there,
281
+ // so "no css on the graph" does NOT imply "no css on the page" in this
282
+ // entry. The DOM check belongs here rather than in the L2 helper — it is
283
+ // environment-specific, and ensureThemeCss must stay importable by the
284
+ // hook-free SSR pipeline.
285
+ if (typeof document === 'undefined' || !document.getElementById('uniweb-theme')) {
286
+ ensureThemeCss(uniweb, foundation)
287
+ }
288
+
277
289
  uniweb.routingComponents = routingComponents
278
290
  uniweb.iconResolver = createIconResolver(content?.icons)
279
291
 
@@ -18,13 +18,14 @@ import React from 'react'
18
18
  import { renderToString } from 'react-dom/server'
19
19
  import { createUniweb, resolveDefaultLocale } from '@uniweb/core'
20
20
  import { sectionDomId } from '@uniweb/core/section-id'
21
- import { buildSectionOverrides } from '@uniweb/theming'
21
+ import { buildSectionOverrides, FONT_LINKS_MARKER } from '@uniweb/theming'
22
22
  import { prepareProps, getComponentMeta } from './prepare-props.js'
23
23
  import { default404Html } from './default-404.js'
24
24
  import {
25
25
  wireFoundationCapabilities,
26
26
  sliceContentForLocale,
27
27
  hydrateDataStore,
28
+ ensureThemeCss,
28
29
  } from './wire-foundation.js'
29
30
  import { resolveLayoutTransitions } from './view-transitions.js'
30
31
  import { renderAppearanceBootScript } from './appearance.js'
@@ -479,6 +480,11 @@ export function initPrerender(content, foundation, extensionsOrOptions, maybeOpt
479
480
  // here vs. setup.js.
480
481
  wireFoundationCapabilities(uniweb, foundation)
481
482
 
483
+ // Site-wide theme CSS. Unconditional here: at this point there is no
484
+ // <head> to inspect, and injectPageContent() emits the result
485
+ // idempotently, so a lane that already baked the style tag is unaffected.
486
+ ensureThemeCss(uniweb, foundation)
487
+
482
488
  // Register SSR-safe routing so useRouting()/useActiveRoute() work during prerender.
483
489
  // renderPage() calls website.setActivePage() before rendering each page,
484
490
  // so activePage.route always reflects the page being rendered.
@@ -664,6 +670,36 @@ export function injectPageContent(html, renderedContent, page, options = {}) {
664
670
  }
665
671
  }
666
672
 
673
+ // Site-wide theme CSS. Derived from the website graph
674
+ // (`website.themeData`), so it belongs on THIS side of the seam — every
675
+ // lane builds that graph. It lived in @uniweb/build's injectBuildData
676
+ // until 2026-07-28, which meant sites served by any lane that doesn't run
677
+ // the framework's build rendered with every semantic token unset: no
678
+ // colours, no backgrounds, no failure anywhere. That is the same mistake
679
+ // the appearance script above was moved out of, four lines below the
680
+ // comment warning about it — see the note in build/src/prerender.js.
681
+ // Idempotent, so a shell that already carries the tag is left alone.
682
+ const themeData = page?.website?.themeData
683
+ const themeCss = themeData?.css
684
+ if (themeCss && !result.includes('id="uniweb-theme"')) {
685
+ result = result.replace(
686
+ '</head>',
687
+ ` <style id="uniweb-theme">\n${themeCss}\n </style>\n</head>`
688
+ )
689
+ }
690
+
691
+ // The theme's font <link> tags — same seam, same reasoning. Graph-derived,
692
+ // so a lane that never runs @uniweb/build still gets its webfonts instead of
693
+ // falling back to system faces. Deduped on FONT_LINKS_MARKER rather than an
694
+ // id because <link> tags have none; the marker is owned by @uniweb/theming,
695
+ // which generates the block, so this and @uniweb/build read one literal.
696
+ if (themeData?.links && !result.includes(FONT_LINKS_MARKER)) {
697
+ result = result.replace(
698
+ '</head>',
699
+ ` ${FONT_LINKS_MARKER}\n${themeData.links}\n</head>`
700
+ )
701
+ }
702
+
667
703
  // Inject per-page section override CSS before </head>
668
704
  if (options.sectionOverrideCSS) {
669
705
  const overrideStyle = `<style id="uniweb-page-overrides">\n${options.sectionOverrideCSS}\n</style>`
@@ -46,6 +46,7 @@
46
46
 
47
47
  import React from 'react'
48
48
  import { deriveCacheKey, resolveDefaultLocale } from '@uniweb/core'
49
+ import { buildTheme } from '@uniweb/theming'
49
50
 
50
51
  /**
51
52
  * Renders unhandled `[#id]` cross-reference markers as plain text. Used
@@ -162,3 +163,72 @@ export function hydrateDataStore(website, fetchedData) {
162
163
  website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data })
163
164
  }
164
165
  }
166
+
167
+ /**
168
+ * Make sure the site's theme CSS exists on the graph, generating it from
169
+ * the authored config when nothing upstream did.
170
+ *
171
+ * **The authored theme config is the source of truth in every lane;
172
+ * generated CSS is a cache of it.** `uniweb build` fills that cache and
173
+ * bakes the result into `<head>`, so this is a no-op on the static lane.
174
+ * A lane that serves a site WITHOUT running the framework's build — a
175
+ * backend-hosted SPA, a cloud shell-mode fallback — carries only the
176
+ * authored `theme.yml` (that is the correct thing for a sync wire to
177
+ * carry: `theme.css` is a build artifact, and with two publishers only
178
+ * one of which computes it, shipping it would make a site's styling
179
+ * depend on who published last). Without this helper those lanes render
180
+ * with every semantic token unset — no colours, no backgrounds.
181
+ *
182
+ * Generating here rather than in a publish step is what keeps the
183
+ * three-ingredient contract true: site + foundation + runtime converge
184
+ * to a *styled* page with no fourth actor. It also stays one
185
+ * implementation — the alternative was re-deriving the OKLCH shade math
186
+ * in another language and keeping the two bit-compatible.
187
+ *
188
+ * L2, not L3: this reads and writes graph state and renders nothing, so
189
+ * it has a single home here and both boot paths call it. **The
190
+ * `@uniweb/theming` import is deliberately static.** An SSR isolate
191
+ * loads a fixed modules map and cannot resolve a chunk graph, so the SSR
192
+ * entry must include the generator statically; a lazy `import()` in the
193
+ * browser entry only would mean two mechanisms for one behaviour,
194
+ * drifting independently. Measured cost of the generator: ~4.9 KB gzip.
195
+ *
196
+ * Foundation-declared vars reach us through
197
+ * `capabilities.vars` — emitted into `dist/entry.js` by
198
+ * `@uniweb/build`'s `generate-entry.js`. Before that existed they lived
199
+ * only in `dist/meta/schema.json` and a theme generated outside the
200
+ * build silently lost every one of them.
201
+ *
202
+ * Callers own the "should I?" question, because it is environment-
203
+ * specific: the browser entry skips this when the document already
204
+ * carries a prerendered `<style id="uniweb-theme">` (regenerating from
205
+ * an already-processed config is wasted work at best), while the SSR
206
+ * entry always runs it and lets `injectPageContent()` emit the result
207
+ * idempotently.
208
+ *
209
+ * @param {import('@uniweb/core').default} uniweb - From createUniweb(...).
210
+ * @param {object} foundation - Loaded foundation module (built shape).
211
+ */
212
+ export function ensureThemeCss(uniweb, foundation) {
213
+ const website = uniweb?.activeWebsite
214
+ const themeData = website?.themeData
215
+ if (!themeData || themeData.css) return
216
+
217
+ const caps = foundation?.default?.capabilities || {}
218
+ try {
219
+ const { config, css, links } = buildTheme(themeData, {
220
+ foundationVars: caps.vars || {},
221
+ base: website.basePath || '/',
222
+ })
223
+ // Merge rather than replace: `config` is the processed superset (it
224
+ // adds `palettes`, normalized `contexts`, resolved `fonts`), so this
225
+ // also gives a build-less lane the same themeData shape the static
226
+ // lane has — Theme.getPalette() and friends start working too.
227
+ Object.assign(themeData, config, { css, links })
228
+ } catch (err) {
229
+ // This runs on the path taken when something upstream has already
230
+ // gone wrong. A degraded render that is still legibly the site beats
231
+ // one that looks broken, but neither is worth a boot crash.
232
+ console.warn('[uniweb] theme CSS generation failed:', err?.message || err)
233
+ }
234
+ }