mnfst-render 0.5.1 → 0.5.2
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/manifest.render.mjs +129 -27
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -1883,6 +1883,36 @@ async function runPrerender(config) {
|
|
|
1883
1883
|
let nextId = 0;
|
|
1884
1884
|
const skipTags = new Set(['MAIN', 'BODY', 'HTML']);
|
|
1885
1885
|
|
|
1886
|
+
// Own MutationObserver registered before any other script on the page.
|
|
1887
|
+
// This guarantees we process DOM additions before Alpine's observer does —
|
|
1888
|
+
// critically, before Alpine's observer calls initTree on newly expanded
|
|
1889
|
+
// Manifest components (preloaded or lazy) and bakes their `:class` state.
|
|
1890
|
+
const installHydrateObserver = () => {
|
|
1891
|
+
if (window.__manifestHydrateObserver || !document.body) return;
|
|
1892
|
+
const obs = new MutationObserver((mutations) => {
|
|
1893
|
+
for (const m of mutations) {
|
|
1894
|
+
if (m.type !== 'childList') continue;
|
|
1895
|
+
for (const node of m.addedNodes) {
|
|
1896
|
+
if (node.nodeType !== 1) continue;
|
|
1897
|
+
try { snapshotSubtree(node); } catch (_) {}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
1902
|
+
window.__manifestHydrateObserver = obs;
|
|
1903
|
+
};
|
|
1904
|
+
if (typeof document !== 'undefined') {
|
|
1905
|
+
if (document.body) {
|
|
1906
|
+
installHydrateObserver();
|
|
1907
|
+
} else {
|
|
1908
|
+
document.addEventListener('DOMContentLoaded', installHydrateObserver, { once: true });
|
|
1909
|
+
// Also try once readyState flips to interactive
|
|
1910
|
+
document.addEventListener('readystatechange', () => {
|
|
1911
|
+
if (document.readyState !== 'loading') installHydrateObserver();
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1886
1916
|
const snapshotElement = (el) => {
|
|
1887
1917
|
if (!el || el.nodeType !== 1) return;
|
|
1888
1918
|
if (el.hasAttribute('data-manifest-hyd-id')) return; // already snapshotted
|
|
@@ -1894,7 +1924,7 @@ async function runPrerender(config) {
|
|
|
1894
1924
|
if (a.name === 'data-manifest-hyd-id') continue;
|
|
1895
1925
|
attrs[a.name] = a.value;
|
|
1896
1926
|
}
|
|
1897
|
-
allSnapshots.push({ id, attrs });
|
|
1927
|
+
allSnapshots.push({ id, tag: el.tagName, attrs });
|
|
1898
1928
|
};
|
|
1899
1929
|
|
|
1900
1930
|
const snapshotElementAndDescendants = (el) => {
|
|
@@ -1943,24 +1973,38 @@ async function runPrerender(config) {
|
|
|
1943
1973
|
window.__manifestHydrateSnapshots = allSnapshots;
|
|
1944
1974
|
};
|
|
1945
1975
|
|
|
1946
|
-
// Wrap Alpine.
|
|
1947
|
-
//
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
1950
|
-
//
|
|
1951
|
-
//
|
|
1976
|
+
// Wrap Alpine.start so the snapshot runs INSIDE the start call, before
|
|
1977
|
+
// Alpine has a chance to walk and mutate the tree. alpine:init as an
|
|
1978
|
+
// external hook proved unreliable — in some configurations it fires after
|
|
1979
|
+
// Alpine has already processed some elements. Alpine.start is the single
|
|
1980
|
+
// synchronous entry point for the initial walk, so wrapping it guarantees
|
|
1981
|
+
// we capture source state before any directive has been applied.
|
|
1982
|
+
//
|
|
1983
|
+
// We also wrap Alpine.initTree for lazy-loaded components that appear in
|
|
1984
|
+
// the DOM after Alpine.start() has completed (fetched by the components
|
|
1985
|
+
// plugin in response to new <x-*> placeholders).
|
|
1986
|
+
//
|
|
1987
|
+
// Both wraps are installed via a defineProperty setter on window.Alpine
|
|
1988
|
+
// so they land the instant Alpine's CDN script does `window.Alpine = ...`.
|
|
1952
1989
|
const wrap = (alpine) => {
|
|
1953
1990
|
if (!alpine || alpine.__manifestRenderWrapped) return;
|
|
1954
|
-
if (typeof alpine.initTree !== 'function') return;
|
|
1955
1991
|
alpine.__manifestRenderWrapped = true;
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1992
|
+
if (typeof alpine.start === 'function') {
|
|
1993
|
+
const originalStart = alpine.start.bind(alpine);
|
|
1994
|
+
alpine.start = function () {
|
|
1995
|
+
try { snapshotSubtree(document.body); } catch (_) { /* graceful */ }
|
|
1996
|
+
return originalStart.apply(this, arguments);
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
if (typeof alpine.initTree === 'function') {
|
|
2000
|
+
const originalInit = alpine.initTree.bind(alpine);
|
|
2001
|
+
alpine.initTree = function (root) {
|
|
2002
|
+
try { snapshotSubtree(root || document.body); } catch (_) { /* graceful */ }
|
|
2003
|
+
return originalInit.apply(this, arguments);
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
1961
2006
|
};
|
|
1962
2007
|
|
|
1963
|
-
// Setter trap: as soon as Alpine is assigned to window, wrap it.
|
|
1964
2008
|
let _Alpine;
|
|
1965
2009
|
try {
|
|
1966
2010
|
Object.defineProperty(window, 'Alpine', {
|
|
@@ -1971,9 +2015,8 @@ async function runPrerender(config) {
|
|
|
1971
2015
|
});
|
|
1972
2016
|
} catch (_) { /* defineProperty failed, fall back to event listeners */ }
|
|
1973
2017
|
|
|
1974
|
-
// Belt-and-braces: also try to wrap on alpine:init events in case some
|
|
1975
|
-
// environment beats the setter. wrap() is idempotent.
|
|
1976
2018
|
if (typeof document !== 'undefined') {
|
|
2019
|
+
// Event-based fallback in case the setter trap missed Alpine assignment.
|
|
1977
2020
|
document.addEventListener('alpine:init', () => wrap(window.Alpine));
|
|
1978
2021
|
document.addEventListener('alpine:initialized', () => wrap(window.Alpine));
|
|
1979
2022
|
}
|
|
@@ -2218,22 +2261,81 @@ async function runPrerender(config) {
|
|
|
2218
2261
|
// it would in the live SPA. After restoring source attributes we re-add the
|
|
2219
2262
|
// `data-prerender-hydrate` marker so downstream Node.js stripping passes
|
|
2220
2263
|
// continue to skip these elements.
|
|
2221
|
-
|
|
2264
|
+
//
|
|
2265
|
+
// Implementation note: we use `outerHTML` to swap the element rather than
|
|
2266
|
+
// `setAttribute` per-attribute. Alpine's special attribute names (`@click`,
|
|
2267
|
+
// possibly others starting with `@`) are not valid DOM Names per the XML
|
|
2268
|
+
// production, so `setAttribute('@click', …)` throws InvalidCharacterError.
|
|
2269
|
+
// The HTML parser, on the other hand, is lenient and accepts these names.
|
|
2270
|
+
// Building an HTML string and assigning it via outerHTML round-trips through
|
|
2271
|
+
// the parser and produces an element with all source attributes intact.
|
|
2272
|
+
// Stop Alpine from observing further DOM mutations and flush any pending
|
|
2273
|
+
// effects. Then restore each hydrate target by replacing it with a fresh
|
|
2274
|
+
// element parsed from a source-attribute HTML string. Replacing the element
|
|
2275
|
+
// (rather than mutating attributes in place) detaches it from Alpine's
|
|
2276
|
+
// reactive bindings entirely — the new node has no `_x_*` state, no
|
|
2277
|
+
// effects, and no observers. Alpine's MutationObserver is stopped first
|
|
2278
|
+
// so it can't pick up the new node and re-process it.
|
|
2279
|
+
//
|
|
2280
|
+
// We process snapshots deepest-first so that when an ancestor is rebuilt,
|
|
2281
|
+
// its children have already been replaced with their pristine versions and
|
|
2282
|
+
// are captured (via innerHTML) into the new ancestor.
|
|
2283
|
+
const restoreReport = await page.evaluate(async () => {
|
|
2284
|
+
try { window.Alpine && window.Alpine.flushAndStopDeferringMutations && window.Alpine.flushAndStopDeferringMutations(); } catch (_) {}
|
|
2285
|
+
try { window.Alpine && window.Alpine.stopObservingMutations && window.Alpine.stopObservingMutations(); } catch (_) {}
|
|
2286
|
+
await Promise.resolve();
|
|
2287
|
+
await Promise.resolve();
|
|
2288
|
+
|
|
2222
2289
|
const snapshots = window.__manifestHydrateSnapshots || [];
|
|
2290
|
+
const report = { total: snapshots.length, restored: 0, notFound: 0, errors: [] };
|
|
2291
|
+
|
|
2292
|
+
// Resolve every snapshot to its element, then sort by depth (deepest first).
|
|
2293
|
+
const items = [];
|
|
2223
2294
|
snapshots.forEach(({ id, attrs }) => {
|
|
2224
2295
|
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2225
|
-
if (!el) return;
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
Object.entries(attrs).forEach(([name, value]) => {
|
|
2230
|
-
el.setAttribute(name, value);
|
|
2231
|
-
});
|
|
2232
|
-
// Marker so downstream stripping (xfor processing, dynamic-binding strip,
|
|
2233
|
-
// component pre-render marking, etc.) skips this element.
|
|
2234
|
-
el.setAttribute('data-prerender-hydrate', '1');
|
|
2296
|
+
if (!el) { report.notFound++; return; }
|
|
2297
|
+
let depth = 0;
|
|
2298
|
+
for (let p = el.parentNode; p; p = p.parentNode) depth++;
|
|
2299
|
+
items.push({ id, el, attrs, depth });
|
|
2235
2300
|
});
|
|
2301
|
+
items.sort((a, b) => b.depth - a.depth);
|
|
2302
|
+
|
|
2303
|
+
const voidEls = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
2304
|
+
const escAttr = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"');
|
|
2305
|
+
|
|
2306
|
+
items.forEach(({ id, attrs }) => {
|
|
2307
|
+
// Re-resolve the element by id every iteration: ancestors that were
|
|
2308
|
+
// already rebuilt will have re-parsed their children, so previous
|
|
2309
|
+
// references are stale.
|
|
2310
|
+
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2311
|
+
if (!el || !el.parentNode) { report.errors.push({ id, msg: 'lost reference' }); return; }
|
|
2312
|
+
const tag = el.tagName.toLowerCase();
|
|
2313
|
+
const attrString = Object.entries(attrs)
|
|
2314
|
+
.map(([name, value]) => `${name}="${escAttr(value)}"`)
|
|
2315
|
+
.join(' ');
|
|
2316
|
+
const innerHTML = voidEls.has(tag) ? '' : el.innerHTML;
|
|
2317
|
+
const newHTML = voidEls.has(tag)
|
|
2318
|
+
? `<${tag} ${attrString} data-prerender-hydrate="1">`
|
|
2319
|
+
: `<${tag} ${attrString} data-prerender-hydrate="1">${innerHTML}</${tag}>`;
|
|
2320
|
+
// Parse via a temporary container so we can use replaceChild (more
|
|
2321
|
+
// reliable than outerHTML in nested-replace scenarios).
|
|
2322
|
+
const tmp = document.createElement(el.parentNode.tagName === 'TR' ? 'tr' : 'div');
|
|
2323
|
+
tmp.innerHTML = newHTML;
|
|
2324
|
+
const parsed = tmp.firstElementChild;
|
|
2325
|
+
if (!parsed) { report.errors.push({ id, msg: 'parse failed' }); return; }
|
|
2326
|
+
try {
|
|
2327
|
+
el.parentNode.replaceChild(parsed, el);
|
|
2328
|
+
report.restored++;
|
|
2329
|
+
} catch (e) {
|
|
2330
|
+
report.errors.push({ id, tag, msg: String(e && e.message || e) });
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
return report;
|
|
2236
2335
|
});
|
|
2336
|
+
if (config.debugPrerender) {
|
|
2337
|
+
pushDebug({ path: displayPath, stage: 'hydrate-restore', metrics: restoreReport });
|
|
2338
|
+
}
|
|
2237
2339
|
|
|
2238
2340
|
// x-for lists: keep static lists in the HTML for SEO; collapse only dynamic lists so Alpine re-renders.
|
|
2239
2341
|
// Explicit: data-prerender="dynamic"|"skip". Inferred: x-for uses $search/$query,
|