@uniweb/runtime 0.28.5 → 0.28.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/dist/ssr.js +6 -0
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/components/PageRenderer.jsx +32 -44
- package/src/default-fetcher.js +3 -1
- package/src/isolate-api.js +16 -6
- package/src/ssr.js +12 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.7",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "^0.35.
|
|
39
|
+
"@uniweb/core": "^0.35.2",
|
|
40
40
|
"@uniweb/theming": "^0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
45
45
|
"vite": "^7.3.1",
|
|
46
46
|
"vitest": "^4.1.7",
|
|
47
|
-
"@uniweb/build": "0.74.
|
|
47
|
+
"@uniweb/build": "0.74.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
|
@@ -122,12 +122,14 @@ export default function PageRenderer() {
|
|
|
122
122
|
// Force re-render counter — incremented by DataStore listener below
|
|
123
123
|
const [, forceUpdate] = useReducer((x) => x + 1, 0)
|
|
124
124
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
125
|
+
// ⭐ WHAT THE URL NAMES — `Website#resolveRoute`, the one rule every lane calls
|
|
126
|
+
// (`@uniweb/core/resolve-route`): a page, a redirect, a page served from elsewhere, or
|
|
127
|
+
// nothing. Resolved synchronously (not in an effect) so child components like Header see the
|
|
128
|
+
// correct activePage during the same render cycle.
|
|
128
129
|
const navigate = useNavigate()
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
const resolution = website?.resolveRoute?.(location.pathname) ?? null
|
|
132
|
+
let page = resolution && resolution.kind !== 'notFound' ? resolution.page : null
|
|
131
133
|
if (page && website) website.setActivePage(location.pathname)
|
|
132
134
|
|
|
133
135
|
// ─── Content loading (split page content) ───
|
|
@@ -155,32 +157,25 @@ export default function PageRenderer() {
|
|
|
155
157
|
|
|
156
158
|
// ─── Compute navigation targets (before hooks, no early returns) ───
|
|
157
159
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// route, so navigating to it raw dropped both the slug translation and the
|
|
175
|
-
// `/<locale>` prefix: visiting `/fr/Guide-de-démarrage-rapide` landed on
|
|
176
|
-
// `/Quick-Start-Guide/From-Idea-to-Website` and served the page in ENGLISH.
|
|
177
|
-
// getLocaleUrl() is the same helper the language switcher uses.
|
|
178
|
-
const autoRedirectRoute = shouldAutoRedirect
|
|
179
|
-
? website?.getLocaleUrl?.(website.activeLocale, navigableRoute) || navigableRoute
|
|
180
|
-
: null
|
|
160
|
+
// The redirect the URL names, as the resolver gives it: an author's `redirect:` — a page of the
|
|
161
|
+
// site as THIS locale shows it, anything else as written; a page with no content to its first
|
|
162
|
+
// descendant with content, in this locale too (the decision canonical, the destination localized —
|
|
163
|
+
// `localeUrl`, the language switcher's rule); or the locale served unprefixed, asked for with its
|
|
164
|
+
// prefix, to the path without it.
|
|
165
|
+
// ⛔ This component worked the first two out for itself until 2026-09-30, and the static build
|
|
166
|
+
// and every host each had a copy.
|
|
167
|
+
const redirectLocation = resolution?.kind === 'redirect' ? resolution.location : null
|
|
168
|
+
|
|
169
|
+
// ⭐ A page served from elsewhere (`rewrite:`) is the HOST's to proxy, before the app loads.
|
|
170
|
+
// Reached by in-app navigation, it is loaded for real so the host can. Reached as the document's
|
|
171
|
+
// own first load (`location.key === 'default'`), the host served the app instead — it does not
|
|
172
|
+
// proxy — so there is no page here: the not-found page, as a static build emits none for it.
|
|
173
|
+
// ⛔ Until 2026-09-30 both reloaded, and on a host that does not proxy that reload never ended.
|
|
174
|
+
const isRewrite = resolution?.kind === 'rewrite'
|
|
175
|
+
const rewriteNotProxied = isRewrite && location.key === 'default'
|
|
181
176
|
|
|
182
177
|
// If no page found, try the 404 page (do NOT fall back to activePage/homepage)
|
|
183
|
-
const isNotFound = !page && !
|
|
178
|
+
const isNotFound = (!page && !redirectLocation) || rewriteNotProxied
|
|
184
179
|
if (isNotFound) {
|
|
185
180
|
page = website?.getNotFoundPage?.() || null
|
|
186
181
|
}
|
|
@@ -194,18 +189,14 @@ export default function PageRenderer() {
|
|
|
194
189
|
// ─── All hooks called unconditionally (React rules of hooks) ───
|
|
195
190
|
|
|
196
191
|
useEffect(() => {
|
|
197
|
-
if (!
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
if (!redirectLocation) return
|
|
193
|
+
// A URL with a scheme leaves the app — another site, or a locale's own domain.
|
|
194
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(redirectLocation)) {
|
|
195
|
+
window.location.replace(redirectLocation)
|
|
200
196
|
} else {
|
|
201
|
-
navigate(
|
|
197
|
+
navigate(redirectLocation, { replace: true })
|
|
202
198
|
}
|
|
203
|
-
}, [
|
|
204
|
-
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (!shouldAutoRedirect) return
|
|
207
|
-
navigate(autoRedirectRoute, { replace: true })
|
|
208
|
-
}, [shouldAutoRedirect, autoRedirectRoute, navigate])
|
|
199
|
+
}, [redirectLocation, navigate])
|
|
209
200
|
|
|
210
201
|
useHeadMeta(headMeta, { siteName })
|
|
211
202
|
|
|
@@ -221,13 +212,10 @@ export default function PageRenderer() {
|
|
|
221
212
|
|
|
222
213
|
// ─── Early returns (after all hooks) ───
|
|
223
214
|
|
|
224
|
-
if (
|
|
225
|
-
if (shouldAutoRedirect) return null
|
|
215
|
+
if (redirectLocation) return null
|
|
226
216
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
// but if it is (e.g., dev mode), do a full page reload to let the host handle it.
|
|
230
|
-
if (page?.rewrite) {
|
|
217
|
+
// An in-app navigation to a rewrite route: load it for real, so a host that proxies can.
|
|
218
|
+
if (isRewrite && !rewriteNotProxied) {
|
|
231
219
|
window.location.reload()
|
|
232
220
|
return null
|
|
233
221
|
}
|
package/src/default-fetcher.js
CHANGED
|
@@ -252,7 +252,9 @@ export function createDefaultFetcher({ basePath = '', dev = false, fetch: fetchI
|
|
|
252
252
|
* Anything the service does not answer (a retired operator) is sent as written and
|
|
253
253
|
* answered there: never approximated.
|
|
254
254
|
*/
|
|
255
|
-
|
|
255
|
+
// Exported so the question a parametric page's record is asked with can be generated as test
|
|
256
|
+
// vectors: it is this body, and nothing else composes one.
|
|
257
|
+
export function toQuestion(request) {
|
|
256
258
|
const q = { schema: request.schema }
|
|
257
259
|
const where = request.where && typeof request.where === 'object' ? request.where : null
|
|
258
260
|
// ⭐ `scope` crosses as authored. ⛔ A top-level `where.path.under` was respelled
|
package/src/isolate-api.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* ⭐ THE ONE PLACE THE NUMBER IS STATED. A host loads the SITE'S pinned runtime as a
|
|
6
6
|
* dynamically-loaded artifact — never an import — so it cannot link-check what it
|
|
7
7
|
* calls; it feature-detects, and an export that is missing looks exactly like an old
|
|
8
|
-
* runtime. The backend evaluates a site's runtime at publish
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* runtime. The backend evaluates a site's runtime at publish against this floor [Diego,
|
|
9
|
+
* 2026-09-04: "We will set a runtime version floor that guarantees they are there"],
|
|
10
|
+
* composing it as `max(floor, foundationFloors)`. It reads the floor from the runtime
|
|
11
|
+
* channel's index (`minUsable`), which every channel publish writes from this file —
|
|
12
|
+
* since 2026-09-05, when that replaced a constant copied from here by hand. (This said
|
|
13
|
+
* the backend held a copied constant until 2026-09-30.) `tests/isolate-api.test.js` is
|
|
14
|
+
* what keeps this file honest: every export of `src/ssr.js` must appear below with the version
|
|
13
15
|
* it first shipped in, and every name below must still be exported — by the source
|
|
14
16
|
* and by the built `dist/ssr.js` when it is present. Forgetting to stamp a new export
|
|
15
17
|
* fails HERE, in the repo where the change happens; a rename fails here too.
|
|
@@ -57,7 +59,9 @@
|
|
|
57
59
|
* 3. replace `UNRELEASED` with the version that publish produced — the floor
|
|
58
60
|
* moves here, and the runtime channel's `minUsable` follows at the
|
|
59
61
|
* next channel publish;
|
|
60
|
-
* 4.
|
|
62
|
+
* 4. nothing to copy by hand: the backend reads the floor from the channel index, so
|
|
63
|
+
* a raise reaches it at the channel publish AFTER the commit that makes it. (This
|
|
64
|
+
* step said "tell backend, which holds the number" until 2026-09-30.)
|
|
61
65
|
*
|
|
62
66
|
* ⚖️ **Step 3 is a real obligation, not bookkeeping**: an export left
|
|
63
67
|
* `UNRELEASED` after it ships keeps the floor below its own API forever, so a
|
|
@@ -106,6 +110,12 @@ export const ISOLATE_API = Object.freeze({
|
|
|
106
110
|
prefetchAndHydrate: '0.14.2',
|
|
107
111
|
// the whole corpus, for a host that indexes rather than renders
|
|
108
112
|
collectSiteRecords: '0.17.0',
|
|
113
|
+
// what a URL names — a page, a redirect, a page served from elsewhere, or nothing
|
|
114
|
+
resolveRoute: '0.28.6',
|
|
115
|
+
routeIndex: '0.28.6',
|
|
116
|
+
localeOfPath: '0.28.6',
|
|
117
|
+
localeUrl: '0.28.6',
|
|
118
|
+
RESOLUTION_STATUS: '0.28.6',
|
|
109
119
|
})
|
|
110
120
|
|
|
111
121
|
/**
|
package/src/ssr.js
CHANGED
|
@@ -63,10 +63,18 @@ export {
|
|
|
63
63
|
// hydration.
|
|
64
64
|
export { loadPageData } from './load-page-data.js'
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
66
|
+
// ⭐ WHAT A URL NAMES — a page, a redirect, a page served from elsewhere, or nothing: the rule the
|
|
67
|
+
// SPA and the static build call (`@uniweb/core/resolve-route`). A host asks it BEFORE it fetches:
|
|
68
|
+
// a redirect needs no data and a URL naming nothing needs no render. It reads one locale's content;
|
|
69
|
+
// `localeOfPath` says which locale a path names, so which content to read. `status` is what a host
|
|
70
|
+
// that sends one sends. ⛔ Each host kept its own copy of the redirect and locale rules until
|
|
71
|
+
// 2026-09-30, and the copies disagreed.
|
|
72
|
+
export { resolveRoute, routeIndex, localeOfPath, localeUrl, RESOLUTION_STATUS } from '@uniweb/core/resolve-route'
|
|
73
|
+
|
|
74
|
+
// Which page a route names, by its exact route and then the parametric patterns
|
|
75
|
+
// (`@uniweb/core/route-match`). ⚠️ The subset of `resolveRoute`: no locale prefix, no translated
|
|
76
|
+
// route, no index page — for a canonical, unprefixed route only. A copy that compared strings made
|
|
77
|
+
// `/about/` prefetch nothing and then render fine (2026-09-12).
|
|
70
78
|
// ⛔ `prefetchPageData`, `resolvePageFetchConfigs` and `executeFetchConfigs` are GONE (2026-09-20).
|
|
71
79
|
// They worked from the payload alone, which cannot say which keys a component declares, which
|
|
72
80
|
// layout a page draws, or which transport a key is routed to — four measured cases wrong
|