mnfst-render 0.4.8 → 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 +150 -25
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -229,23 +229,18 @@ function resolveConfig() {
|
|
|
229
229
|
locales: pre.locales,
|
|
230
230
|
redirects: Array.isArray(pre.redirects) ? pre.redirects : [],
|
|
231
231
|
wait: cli.wait ?? pre.wait ?? null,
|
|
232
|
-
waitAfterIdle:
|
|
232
|
+
waitAfterIdle: 0,
|
|
233
233
|
concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? Math.max(4, cpus().length - 1)),
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
localeSubstitution: pre.localeSubstitution !== false,
|
|
237
|
-
/** Locales to always render with Puppeteer even when localeSubstitution is enabled (e.g. RTL). */
|
|
238
|
-
localeSubstitutionExclude: Array.isArray(pre.localeSubstitutionExclude)
|
|
239
|
-
? pre.localeSubstitutionExclude.map(String)
|
|
240
|
-
: [],
|
|
234
|
+
localeSubstitution: true,
|
|
235
|
+
localeSubstitutionExclude: [],
|
|
241
236
|
/** Explicit locale-neutral paths to render in addition to those discovered automatically.
|
|
242
237
|
* Each entry is expanded to all locale variants (e.g. "legal/privacy" → "cs/legal/privacy", ...) */
|
|
243
238
|
paths: Array.isArray(pre.paths)
|
|
244
239
|
? pre.paths.map((p) => String(p).replace(/^\/+|\/+$/g, '')).filter(Boolean)
|
|
245
240
|
: [],
|
|
246
241
|
dryRun: !!cli.dryRun,
|
|
247
|
-
debugPrerender: !!
|
|
248
|
-
pipelineTimeout:
|
|
242
|
+
debugPrerender: !!cli.debugPrerender,
|
|
243
|
+
pipelineTimeout: 25000,
|
|
249
244
|
};
|
|
250
245
|
}
|
|
251
246
|
|
|
@@ -694,13 +689,11 @@ function promptContinueWithRuntimeTailwind(rootDir) {
|
|
|
694
689
|
|
|
695
690
|
/**
|
|
696
691
|
* Build a static Tailwind stylesheet via @tailwindcss/cli (v4+), scanning project sources.
|
|
697
|
-
* Only runs when the project
|
|
692
|
+
* Only runs when the project uses data-tailwind on the manifest script tag (auto-detected).
|
|
693
|
+
* Set manifest.prerender.tailwindInput to a custom CSS entry file if needed.
|
|
698
694
|
*/
|
|
699
695
|
function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
700
|
-
|
|
701
|
-
if (explicit === false) return false;
|
|
702
|
-
const usesTailwind = explicit === true || indexHtmlUsesTailwind(rootDir);
|
|
703
|
-
if (!usesTailwind) return false;
|
|
696
|
+
if (!indexHtmlUsesTailwind(rootDir)) return false;
|
|
704
697
|
|
|
705
698
|
const outCss = join(outputDir, 'prerender.tailwind.css');
|
|
706
699
|
try {
|
|
@@ -708,7 +701,7 @@ function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
|
708
701
|
} catch {
|
|
709
702
|
const proceed = promptContinueWithRuntimeTailwind(rootDir);
|
|
710
703
|
if (!proceed) {
|
|
711
|
-
throw new Error('prerender aborted: install tailwindcss/@tailwindcss/cli or
|
|
704
|
+
throw new Error('prerender aborted: install tailwindcss/@tailwindcss/cli or remove data-tailwind from your manifest script tag.');
|
|
712
705
|
}
|
|
713
706
|
process.stdout.write('prerender: continuing with runtime data-tailwind behavior.\n');
|
|
714
707
|
return false;
|
|
@@ -726,15 +719,12 @@ function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
|
726
719
|
}
|
|
727
720
|
|
|
728
721
|
const outputBasename = basename(outputDir);
|
|
729
|
-
const
|
|
722
|
+
const contentGlobs = [
|
|
730
723
|
'**/*.html',
|
|
731
724
|
'!**/node_modules/**',
|
|
732
725
|
'!**/dist/**',
|
|
733
726
|
`!**/${outputBasename}/**`,
|
|
734
727
|
];
|
|
735
|
-
const contentGlobs = Array.isArray(pre?.tailwindContent) && pre.tailwindContent.length > 0
|
|
736
|
-
? pre.tailwindContent
|
|
737
|
-
: defaultContent;
|
|
738
728
|
|
|
739
729
|
const args = [
|
|
740
730
|
'--yes',
|
|
@@ -762,7 +752,7 @@ function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
|
762
752
|
}
|
|
763
753
|
}
|
|
764
754
|
if (r.status !== 0) {
|
|
765
|
-
console.error('prerender: Tailwind CLI failed; install with `npm i -D tailwindcss @tailwindcss/cli` or
|
|
755
|
+
console.error('prerender: Tailwind CLI failed; install with `npm i -D tailwindcss @tailwindcss/cli` or check tailwindInput in manifest.prerender.');
|
|
766
756
|
if (r.stderr) console.error(r.stderr);
|
|
767
757
|
if (r.stdout) console.error(r.stdout);
|
|
768
758
|
return false;
|
|
@@ -957,6 +947,13 @@ function stripPrerenderHydrateMarkers(html) {
|
|
|
957
947
|
return html.replace(/\sdata-prerender-hydrate(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
|
|
958
948
|
}
|
|
959
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
|
+
|
|
960
957
|
function markPrerenderedManifestComponents(html) {
|
|
961
958
|
return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
|
|
962
959
|
const a = attrs || '';
|
|
@@ -1345,6 +1342,7 @@ function generateLocaleVariantHtml({
|
|
|
1345
1342
|
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
1346
1343
|
html = markPrerenderedManifestComponents(html);
|
|
1347
1344
|
html = stripPrerenderHydrateMarkers(html);
|
|
1345
|
+
html = stripPrerenderHydrateSnapshotIds(html);
|
|
1348
1346
|
|
|
1349
1347
|
const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
|
|
1350
1348
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
@@ -1867,6 +1865,104 @@ async function runPrerender(config) {
|
|
|
1867
1865
|
}
|
|
1868
1866
|
}, currentLocale);
|
|
1869
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
|
+
|
|
1870
1966
|
pushDebug({ path: displayPath, stage: 'start' });
|
|
1871
1967
|
await page.goto(url, {
|
|
1872
1968
|
waitUntil: 'domcontentloaded',
|
|
@@ -2087,11 +2183,39 @@ async function runPrerender(config) {
|
|
|
2087
2183
|
});
|
|
2088
2184
|
});
|
|
2089
2185
|
|
|
2090
|
-
//
|
|
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.
|
|
2190
|
+
await page.evaluate(() => {
|
|
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.
|
|
2091
2205
|
await page.evaluate(() => {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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');
|
|
2095
2219
|
});
|
|
2096
2220
|
});
|
|
2097
2221
|
|
|
@@ -2438,6 +2562,7 @@ async function runPrerender(config) {
|
|
|
2438
2562
|
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
2439
2563
|
html = markPrerenderedManifestComponents(html);
|
|
2440
2564
|
html = stripPrerenderHydrateMarkers(html);
|
|
2565
|
+
html = stripPrerenderHydrateSnapshotIds(html);
|
|
2441
2566
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
2442
2567
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
2443
2568
|
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
|