mnfst-render 0.4.9 → 0.5.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/manifest.render.mjs +139 -4
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -947,6 +947,13 @@ function stripPrerenderHydrateMarkers(html) {
|
|
|
947
947
|
return html.replace(/\sdata-prerender-hydrate(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
// Remove the snapshot id attribute used by the hydrate restore phase. These ids
|
|
951
|
+
// only exist to let the post-Alpine restore step in Puppeteer find each snapshotted
|
|
952
|
+
// element back; they have no purpose in the final output.
|
|
953
|
+
function stripPrerenderHydrateSnapshotIds(html) {
|
|
954
|
+
return html.replace(/\sdata-manifest-hyd-id(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
|
|
955
|
+
}
|
|
956
|
+
|
|
950
957
|
function markPrerenderedManifestComponents(html) {
|
|
951
958
|
return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
|
|
952
959
|
const a = attrs || '';
|
|
@@ -1335,6 +1342,7 @@ function generateLocaleVariantHtml({
|
|
|
1335
1342
|
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
1336
1343
|
html = markPrerenderedManifestComponents(html);
|
|
1337
1344
|
html = stripPrerenderHydrateMarkers(html);
|
|
1345
|
+
html = stripPrerenderHydrateSnapshotIds(html);
|
|
1338
1346
|
|
|
1339
1347
|
const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
|
|
1340
1348
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
@@ -1857,6 +1865,104 @@ async function runPrerender(config) {
|
|
|
1857
1865
|
}
|
|
1858
1866
|
}, currentLocale);
|
|
1859
1867
|
|
|
1868
|
+
// Snapshot pristine source attributes of hydrate-target elements BEFORE Alpine
|
|
1869
|
+
// touches them. We do this by wrapping `Alpine.initTree` — Alpine calls this
|
|
1870
|
+
// for the initial tree walk AND every time the components plugin lazy-loads a
|
|
1871
|
+
// new <x-*> component. Right before Alpine processes a subtree, we walk it
|
|
1872
|
+
// and snapshot every hydrate target inside. This is the exact moment the
|
|
1873
|
+
// user's source HTML is sitting in the DOM with no Alpine mutations applied.
|
|
1874
|
+
//
|
|
1875
|
+
// The snapshots are restored in a later page.evaluate call after Alpine
|
|
1876
|
+
// settles. This is true hydration: Alpine never gets to bake state into
|
|
1877
|
+
// hydrate elements, so every directive (`:class`, `:style`, `x-text`, custom
|
|
1878
|
+
// plugin directives, etc.) works in the prerendered MPA exactly the way it
|
|
1879
|
+
// does in the live SPA — no per-binding strip logic, no cloak band-aids, no
|
|
1880
|
+
// edge cases to chase.
|
|
1881
|
+
await page.evaluateOnNewDocument(() => {
|
|
1882
|
+
const allSnapshots = [];
|
|
1883
|
+
let nextId = 0;
|
|
1884
|
+
const skipTags = new Set(['MAIN', 'BODY', 'HTML']);
|
|
1885
|
+
|
|
1886
|
+
const snapshotElement = (el) => {
|
|
1887
|
+
if (!el || el.nodeType !== 1) return;
|
|
1888
|
+
if (el.hasAttribute('data-manifest-hyd-id')) return; // already snapshotted
|
|
1889
|
+
const id = '__manifest-hyd-' + nextId++;
|
|
1890
|
+
el.setAttribute('data-manifest-hyd-id', id);
|
|
1891
|
+
const attrs = {};
|
|
1892
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
1893
|
+
const a = el.attributes[i];
|
|
1894
|
+
if (a.name === 'data-manifest-hyd-id') continue;
|
|
1895
|
+
attrs[a.name] = a.value;
|
|
1896
|
+
}
|
|
1897
|
+
allSnapshots.push({ id, attrs });
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
const snapshotElementAndDescendants = (el) => {
|
|
1901
|
+
snapshotElement(el);
|
|
1902
|
+
if (el && el.querySelectorAll) {
|
|
1903
|
+
el.querySelectorAll('*').forEach(snapshotElement);
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
const snapshotSubtree = (root) => {
|
|
1908
|
+
if (!root || root.nodeType !== 1) return;
|
|
1909
|
+
|
|
1910
|
+
// 1. Direct data-hydrate roots + descendants within this subtree.
|
|
1911
|
+
const hydrateRoots = [];
|
|
1912
|
+
if (root.matches && root.matches('[data-hydrate]')) hydrateRoots.push(root);
|
|
1913
|
+
if (root.querySelectorAll) {
|
|
1914
|
+
root.querySelectorAll('[data-hydrate]').forEach((el) => hydrateRoots.push(el));
|
|
1915
|
+
}
|
|
1916
|
+
hydrateRoots.forEach(snapshotElementAndDescendants);
|
|
1917
|
+
|
|
1918
|
+
// 2. x-theme elements (color mode plugin needs runtime click handler).
|
|
1919
|
+
if (root.matches && root.matches('[x-theme]')) snapshotElementAndDescendants(root);
|
|
1920
|
+
if (root.querySelectorAll) {
|
|
1921
|
+
root.querySelectorAll('[x-theme]').forEach(snapshotElementAndDescendants);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// 3. Propagate from data-hydrate children to nearest LOCAL x-data ancestor
|
|
1925
|
+
// so the reactive controller, sibling event handlers (@click toggles
|
|
1926
|
+
// etc.) and all bindings inside the scope are preserved together.
|
|
1927
|
+
// Skip page-level scopes (main, body, [x-route]).
|
|
1928
|
+
hydrateRoots.forEach((el) => {
|
|
1929
|
+
let ancestor = el.parentElement;
|
|
1930
|
+
while (ancestor && ancestor !== document.body) {
|
|
1931
|
+
if (
|
|
1932
|
+
ancestor.hasAttribute('x-data') &&
|
|
1933
|
+
!skipTags.has(ancestor.tagName) &&
|
|
1934
|
+
!ancestor.hasAttribute('x-route')
|
|
1935
|
+
) {
|
|
1936
|
+
snapshotElementAndDescendants(ancestor);
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
ancestor = ancestor.parentElement;
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
window.__manifestHydrateSnapshots = allSnapshots;
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
// Wrap Alpine.initTree once Alpine is available. alpine:init fires before
|
|
1947
|
+
// Alpine walks the tree for the first time, giving us the perfect insertion
|
|
1948
|
+
// point. After wrapping, every initTree call (initial walk + every lazy
|
|
1949
|
+
// component load) snapshots its subtree before Alpine processes it.
|
|
1950
|
+
let installed = false;
|
|
1951
|
+
const installInterceptor = () => {
|
|
1952
|
+
if (installed || !window.Alpine || typeof window.Alpine.initTree !== 'function') return;
|
|
1953
|
+
installed = true;
|
|
1954
|
+
const original = window.Alpine.initTree.bind(window.Alpine);
|
|
1955
|
+
window.Alpine.initTree = function (root) {
|
|
1956
|
+
try { snapshotSubtree(root || document.body); } catch (_) { /* graceful */ }
|
|
1957
|
+
return original.apply(this, arguments);
|
|
1958
|
+
};
|
|
1959
|
+
};
|
|
1960
|
+
if (typeof document !== 'undefined') {
|
|
1961
|
+
document.addEventListener('alpine:init', installInterceptor);
|
|
1962
|
+
document.addEventListener('alpine:initialized', installInterceptor);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1860
1966
|
pushDebug({ path: displayPath, stage: 'start' });
|
|
1861
1967
|
await page.goto(url, {
|
|
1862
1968
|
waitUntil: 'domcontentloaded',
|
|
@@ -2077,11 +2183,39 @@ async function runPrerender(config) {
|
|
|
2077
2183
|
});
|
|
2078
2184
|
});
|
|
2079
2185
|
|
|
2080
|
-
//
|
|
2186
|
+
// Strip x-markdown from elements that already have baked content.
|
|
2187
|
+
// The markdown plugin hides elements with opacity:0 on init, then re-fetches and re-renders.
|
|
2188
|
+
// For prerendered pages the content is already baked — removing x-markdown prevents the
|
|
2189
|
+
// runtime plugin from re-processing (and temporarily hiding) the static content.
|
|
2081
2190
|
await page.evaluate(() => {
|
|
2082
|
-
document.querySelectorAll('[
|
|
2083
|
-
|
|
2084
|
-
|
|
2191
|
+
document.querySelectorAll('[x-markdown]').forEach((el) => {
|
|
2192
|
+
if (!el.textContent.trim() && !el.innerHTML.trim()) return;
|
|
2193
|
+
el.removeAttribute('x-markdown');
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
// Restore hydrate-target elements to their pristine source attributes
|
|
2198
|
+
// (snapshotted via evaluateOnNewDocument before Alpine ran). This is true
|
|
2199
|
+
// hydration: every Alpine binding (`:class`, `:style`, `:value`, `x-text`,
|
|
2200
|
+
// `x-init`, custom plugin directives, …) is preserved exactly as authored,
|
|
2201
|
+
// and Alpine processes them at runtime in the prerendered MPA the same way
|
|
2202
|
+
// it would in the live SPA. After restoring source attributes we re-add the
|
|
2203
|
+
// `data-prerender-hydrate` marker so downstream Node.js stripping passes
|
|
2204
|
+
// continue to skip these elements.
|
|
2205
|
+
await page.evaluate(() => {
|
|
2206
|
+
const snapshots = window.__manifestHydrateSnapshots || [];
|
|
2207
|
+
snapshots.forEach(({ id, attrs }) => {
|
|
2208
|
+
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2209
|
+
if (!el) return;
|
|
2210
|
+
// Wipe everything Alpine may have added or mutated.
|
|
2211
|
+
Array.from(el.attributes).map((a) => a.name).forEach((name) => el.removeAttribute(name));
|
|
2212
|
+
// Put the original source attributes back.
|
|
2213
|
+
Object.entries(attrs).forEach(([name, value]) => {
|
|
2214
|
+
el.setAttribute(name, value);
|
|
2215
|
+
});
|
|
2216
|
+
// Marker so downstream stripping (xfor processing, dynamic-binding strip,
|
|
2217
|
+
// component pre-render marking, etc.) skips this element.
|
|
2218
|
+
el.setAttribute('data-prerender-hydrate', '1');
|
|
2085
2219
|
});
|
|
2086
2220
|
});
|
|
2087
2221
|
|
|
@@ -2428,6 +2562,7 @@ async function runPrerender(config) {
|
|
|
2428
2562
|
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
2429
2563
|
html = markPrerenderedManifestComponents(html);
|
|
2430
2564
|
html = stripPrerenderHydrateMarkers(html);
|
|
2565
|
+
html = stripPrerenderHydrateSnapshotIds(html);
|
|
2431
2566
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
2432
2567
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
2433
2568
|
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
|