mnfst-render 0.4.9 → 0.5.1
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 +155 -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,120 @@ 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. This MUST happen before any plugin calls initTree —
|
|
1947
|
+
// otherwise preloaded components (loaded eagerly by manifest.components before
|
|
1948
|
+
// Alpine starts walking the tree) get processed by the original initTree and
|
|
1949
|
+
// their hydrate targets are missed. We use a defineProperty setter on
|
|
1950
|
+
// window.Alpine so the wrap happens the instant Alpine's CDN script does
|
|
1951
|
+
// `window.Alpine = ...` — earlier than any event we could listen for.
|
|
1952
|
+
const wrap = (alpine) => {
|
|
1953
|
+
if (!alpine || alpine.__manifestRenderWrapped) return;
|
|
1954
|
+
if (typeof alpine.initTree !== 'function') return;
|
|
1955
|
+
alpine.__manifestRenderWrapped = true;
|
|
1956
|
+
const original = alpine.initTree.bind(alpine);
|
|
1957
|
+
alpine.initTree = function (root) {
|
|
1958
|
+
try { snapshotSubtree(root || document.body); } catch (_) { /* graceful */ }
|
|
1959
|
+
return original.apply(this, arguments);
|
|
1960
|
+
};
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
// Setter trap: as soon as Alpine is assigned to window, wrap it.
|
|
1964
|
+
let _Alpine;
|
|
1965
|
+
try {
|
|
1966
|
+
Object.defineProperty(window, 'Alpine', {
|
|
1967
|
+
configurable: true,
|
|
1968
|
+
enumerable: true,
|
|
1969
|
+
get() { return _Alpine; },
|
|
1970
|
+
set(v) { _Alpine = v; wrap(v); },
|
|
1971
|
+
});
|
|
1972
|
+
} catch (_) { /* defineProperty failed, fall back to event listeners */ }
|
|
1973
|
+
|
|
1974
|
+
// Belt-and-braces: also try to wrap on alpine:init events in case some
|
|
1975
|
+
// environment beats the setter. wrap() is idempotent.
|
|
1976
|
+
if (typeof document !== 'undefined') {
|
|
1977
|
+
document.addEventListener('alpine:init', () => wrap(window.Alpine));
|
|
1978
|
+
document.addEventListener('alpine:initialized', () => wrap(window.Alpine));
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1860
1982
|
pushDebug({ path: displayPath, stage: 'start' });
|
|
1861
1983
|
await page.goto(url, {
|
|
1862
1984
|
waitUntil: 'domcontentloaded',
|
|
@@ -2077,11 +2199,39 @@ async function runPrerender(config) {
|
|
|
2077
2199
|
});
|
|
2078
2200
|
});
|
|
2079
2201
|
|
|
2080
|
-
//
|
|
2202
|
+
// Strip x-markdown from elements that already have baked content.
|
|
2203
|
+
// The markdown plugin hides elements with opacity:0 on init, then re-fetches and re-renders.
|
|
2204
|
+
// For prerendered pages the content is already baked — removing x-markdown prevents the
|
|
2205
|
+
// runtime plugin from re-processing (and temporarily hiding) the static content.
|
|
2206
|
+
await page.evaluate(() => {
|
|
2207
|
+
document.querySelectorAll('[x-markdown]').forEach((el) => {
|
|
2208
|
+
if (!el.textContent.trim() && !el.innerHTML.trim()) return;
|
|
2209
|
+
el.removeAttribute('x-markdown');
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
// Restore hydrate-target elements to their pristine source attributes
|
|
2214
|
+
// (snapshotted via evaluateOnNewDocument before Alpine ran). This is true
|
|
2215
|
+
// hydration: every Alpine binding (`:class`, `:style`, `:value`, `x-text`,
|
|
2216
|
+
// `x-init`, custom plugin directives, …) is preserved exactly as authored,
|
|
2217
|
+
// and Alpine processes them at runtime in the prerendered MPA the same way
|
|
2218
|
+
// it would in the live SPA. After restoring source attributes we re-add the
|
|
2219
|
+
// `data-prerender-hydrate` marker so downstream Node.js stripping passes
|
|
2220
|
+
// continue to skip these elements.
|
|
2081
2221
|
await page.evaluate(() => {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2222
|
+
const snapshots = window.__manifestHydrateSnapshots || [];
|
|
2223
|
+
snapshots.forEach(({ id, attrs }) => {
|
|
2224
|
+
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2225
|
+
if (!el) return;
|
|
2226
|
+
// Wipe everything Alpine may have added or mutated.
|
|
2227
|
+
Array.from(el.attributes).map((a) => a.name).forEach((name) => el.removeAttribute(name));
|
|
2228
|
+
// Put the original source attributes back.
|
|
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');
|
|
2085
2235
|
});
|
|
2086
2236
|
});
|
|
2087
2237
|
|
|
@@ -2428,6 +2578,7 @@ async function runPrerender(config) {
|
|
|
2428
2578
|
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
2429
2579
|
html = markPrerenderedManifestComponents(html);
|
|
2430
2580
|
html = stripPrerenderHydrateMarkers(html);
|
|
2581
|
+
html = stripPrerenderHydrateSnapshotIds(html);
|
|
2431
2582
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
2432
2583
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
2433
2584
|
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
|