@opentf/web 0.8.0 → 0.9.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 +1 -1
- package/runtime/dom.js +20 -0
- package/runtime/hydrate.js +43 -1
- package/runtime/router.js +6 -0
package/package.json
CHANGED
package/runtime/dom.js
CHANGED
|
@@ -147,6 +147,26 @@ export function setAttr(el, name, value) {
|
|
|
147
147
|
* component picks it up from `getAttribute` when it upgrades.
|
|
148
148
|
*/
|
|
149
149
|
export function setProp(el, name, value) {
|
|
150
|
+
// A component prop must land as a *property* (carrying the rich value), never a
|
|
151
|
+
// stringified attribute. A server-parsed custom element that a parent's adopt walk
|
|
152
|
+
// reaches while it is still inert — its own `customElements.define`-driven upgrade
|
|
153
|
+
// hasn't run yet (synchronous upgrades fire in document order, so a parent upgrades
|
|
154
|
+
// and runs this before its later-in-DOM children) — has no accessors, so `name in el`
|
|
155
|
+
// is false. Falling back to `setAttr` would stringify an object to "[object Object]"
|
|
156
|
+
// and, via `attributeChangedCallback`, clobber the child's payload-hydrated signal —
|
|
157
|
+
// flipping conditional branches and desyncing the adopt walk. Force the pending upgrade
|
|
158
|
+
// first so the property setter exists; it's a no-op for already-upgraded or non-custom
|
|
159
|
+
// elements. (A client CSR build creates already-upgraded instances, so this never fires
|
|
160
|
+
// there.)
|
|
161
|
+
if (
|
|
162
|
+
!(name in el) &&
|
|
163
|
+
el.tagName &&
|
|
164
|
+
el.tagName.includes("-") &&
|
|
165
|
+
typeof customElements !== "undefined" &&
|
|
166
|
+
customElements.get(el.tagName.toLowerCase())
|
|
167
|
+
) {
|
|
168
|
+
customElements.upgrade(el);
|
|
169
|
+
}
|
|
150
170
|
if (name in el) el[name] = value;
|
|
151
171
|
else setAttr(el, name, value);
|
|
152
172
|
}
|
package/runtime/hydrate.js
CHANGED
|
@@ -58,13 +58,55 @@ export class HydrationMismatch extends Error {
|
|
|
58
58
|
// `firstChild`, so the structural test alone can't tell "adopt the server DOM" from
|
|
59
59
|
// "build and slot these children" (it mis-adopts on plain SPA navigation). The flag is
|
|
60
60
|
// the unambiguous signal — it is only ever set during the one-shot first-paint pass.
|
|
61
|
-
|
|
61
|
+
//
|
|
62
|
+
// Why it initializes from the DOM sentinel, not `false`: a custom element upgrades — and
|
|
63
|
+
// its `connectedCallback` runs the build-vs-adopt switch — the instant its class is
|
|
64
|
+
// `customElements.define`d. Framework components imported *eagerly* by the app entry
|
|
65
|
+
// (e.g. `<Link>` from `@opentf/web`) are therefore defined and upgraded during the entry
|
|
66
|
+
// bundle's evaluation, **before** `mountApp` runs `beginHydration()`. If the flag were
|
|
67
|
+
// still `false` then, every server-rendered `<web-link>` would take the *build* arm —
|
|
68
|
+
// re-wrapping the server subtree it should have adopted (a silent `<a><a>` double-build)
|
|
69
|
+
// — long before the router ever starts hydrating. So the flag is seeded synchronously
|
|
70
|
+
// at module load from the server sentinel (`[data-otfw-hydrate]`, stamped only when the
|
|
71
|
+
// page shipped adoptable server markup): the deferred entry module evaluates after the
|
|
72
|
+
// HTML is parsed, so the sentinel is present, and this module is a transitive dependency
|
|
73
|
+
// of every component (via the runtime), so it evaluates before any component's `define`.
|
|
74
|
+
// `mountApp` still brackets the pass with `beginHydration`/`endHydration`, and clears the
|
|
75
|
+
// flag when it decides *not* to hydrate (no sentinel / empty root), so a CSR mount and
|
|
76
|
+
// every subsequent SPA navigation build fresh.
|
|
77
|
+
let _hydrating =
|
|
78
|
+
typeof document !== "undefined" && !!document.querySelector("[data-otfw-hydrate]");
|
|
62
79
|
|
|
63
80
|
/** Is the client mid-hydration right now? */
|
|
64
81
|
export function isHydrating() {
|
|
65
82
|
return _hydrating;
|
|
66
83
|
}
|
|
67
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Run `fn` with the hydration flag *cleared*, restoring the prior value (nesting-safe).
|
|
87
|
+
*
|
|
88
|
+
* The build/adopt decision hangs on one module-global flag, but the invariant it must
|
|
89
|
+
* encode is narrower: `isHydrating()` may be true only while the DOM being processed is
|
|
90
|
+
* the server's. The moment a component builds fresh DOM — because its view isn't
|
|
91
|
+
* adoptable (`RebuildIfServerChildren`), or its adopt hit a mismatch and it's recovering,
|
|
92
|
+
* or it's a nested build — that fresh subtree is *not* server-rendered. Its child custom
|
|
93
|
+
* elements upgrade synchronously during `appendChild` inside this build, and if they still
|
|
94
|
+
* saw `isHydrating()` true they'd try to *adopt* content their parent just `createElement`'d
|
|
95
|
+
* → a guaranteed `HydrationMismatch` cascade (a non-adoptable layout would tear the whole
|
|
96
|
+
* page's islands into rebuild-storms). Bracketing every build path with this makes the
|
|
97
|
+
* children build too, matching the DOM they're actually handed. Synchronous only — builds
|
|
98
|
+
* never await — so a plain global save/restore is correct even when builds nest.
|
|
99
|
+
*/
|
|
100
|
+
export function runBuild(fn) {
|
|
101
|
+
const prev = _hydrating;
|
|
102
|
+
_hydrating = false;
|
|
103
|
+
try {
|
|
104
|
+
return fn();
|
|
105
|
+
} finally {
|
|
106
|
+
_hydrating = prev;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
68
110
|
/** Run `fn` with the hydration flag set, restoring the prior value (nesting-safe).
|
|
69
111
|
* Synchronous only — for the async first-paint pass use {@link beginHydration} /
|
|
70
112
|
* {@link endHydration}, which span the route module's `import()`. */
|
package/runtime/router.js
CHANGED
|
@@ -504,6 +504,12 @@ export function mountApp({ pages, target, guard: g, i18n, nav, loaders } = {}) {
|
|
|
504
504
|
typeof rootEl.hasAttribute === "function" &&
|
|
505
505
|
rootEl.hasAttribute("data-otfw-hydrate")
|
|
506
506
|
);
|
|
507
|
+
// The flag is seeded `true` from the sentinel at module load (so eagerly-defined
|
|
508
|
+
// framework components adopt on upgrade — see hydrate.js). If this mount is NOT
|
|
509
|
+
// hydrating after all (no sentinel, or an empty root), clear it now so the CSR build
|
|
510
|
+
// below — and every later navigation — builds fresh. The hydrate path clears it itself
|
|
511
|
+
// (via `endHydration` in `navigate`'s `finally`) once first paint is adopted.
|
|
512
|
+
if (!hydrate) endHydration();
|
|
507
513
|
return navigate(
|
|
508
514
|
window.location.pathname + window.location.search + window.location.hash,
|
|
509
515
|
true,
|