mnfst-render 0.3.7 → 0.3.9
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 +92 -25
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -252,6 +252,13 @@ function loadConfig(rootDir) {
|
|
|
252
252
|
return manifest;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
function normalizeLocaleRouteExclude(val) {
|
|
256
|
+
if (val == null) return [];
|
|
257
|
+
if (Array.isArray(val)) return val.map((s) => String(s).trim()).filter(Boolean);
|
|
258
|
+
if (typeof val === 'string') return val.split(',').map((s) => s.trim()).filter(Boolean);
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
|
|
255
262
|
function resolveConfig() {
|
|
256
263
|
const cli = parseArgs();
|
|
257
264
|
const cwd = process.cwd();
|
|
@@ -274,6 +281,10 @@ function resolveConfig() {
|
|
|
274
281
|
output: resolve(root, cli.output ?? pre.output ?? 'website'),
|
|
275
282
|
root,
|
|
276
283
|
routerBase: pre.routerBase ?? null,
|
|
284
|
+
/** Logical path prefixes (after locale) that skip sticky locale prefix; see manifest:locale-route-exclude */
|
|
285
|
+
localeRouteExclude: normalizeLocaleRouteExclude(
|
|
286
|
+
pre.localeRouteExclude ?? pre.localeStickyExclude
|
|
287
|
+
),
|
|
277
288
|
locales: pre.locales,
|
|
278
289
|
redirects: Array.isArray(pre.redirects) ? pre.redirects : [],
|
|
279
290
|
wait: cli.wait ?? pre.wait ?? null,
|
|
@@ -853,19 +864,26 @@ function depthFromOutputRoot(outputDir, filePath) {
|
|
|
853
864
|
return rel.split(sep).filter(Boolean).length;
|
|
854
865
|
}
|
|
855
866
|
|
|
856
|
-
/**
|
|
857
|
-
function
|
|
867
|
+
/** Root-absolute path for prerender bundles (same URL from every page depth; supports manifest:router-base). */
|
|
868
|
+
function buildRootAssetPath(routerBasePath, filename) {
|
|
869
|
+
const base = String(routerBasePath || '').replace(/^\/+|\/+$/g, '');
|
|
870
|
+
const name = String(filename || '').replace(/^\/+/, '');
|
|
871
|
+
const path = base ? `${base}/${name}` : name;
|
|
872
|
+
return '/' + path.replace(/\/{2,}/g, '/');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** Inject stylesheet link with root-absolute href (avoids ../ resolving under locale segments like /en/page/). */
|
|
876
|
+
function postProcessInjectStylesheetLink(outputDir, filename, routerBasePath) {
|
|
858
877
|
const cssPath = join(outputDir, filename);
|
|
859
878
|
if (!existsSync(cssPath)) return;
|
|
860
879
|
const stat = statSync(cssPath);
|
|
861
880
|
if (stat.size === 0) return;
|
|
862
881
|
|
|
882
|
+
const href = buildRootAssetPath(routerBasePath, filename);
|
|
883
|
+
const tag = `<link rel="stylesheet" href="${href}">`;
|
|
863
884
|
const files = walkHtmlFiles(outputDir);
|
|
864
885
|
for (const file of files) {
|
|
865
886
|
let html = readFileSync(file, 'utf8');
|
|
866
|
-
const depth = depthFromOutputRoot(outputDir, file);
|
|
867
|
-
const prefix = depth ? '../'.repeat(depth) : '';
|
|
868
|
-
const tag = `<link rel="stylesheet" href="${prefix}${filename}">`;
|
|
869
887
|
html = injectBeforeHeadClose(html, tag);
|
|
870
888
|
writeFileSync(file, html, 'utf8');
|
|
871
889
|
}
|
|
@@ -879,6 +897,10 @@ function stripDuplicatedLoopDirectives(html) {
|
|
|
879
897
|
return html;
|
|
880
898
|
}
|
|
881
899
|
|
|
900
|
+
function isHydrateMarkedAttrs(attrsStr) {
|
|
901
|
+
return /\sdata-prerender-hydrate(?:\s*=|[\s>])/i.test(attrsStr || '');
|
|
902
|
+
}
|
|
903
|
+
|
|
882
904
|
// --- Strip x-text and x-html that reference $x when static/SEO (content already in snapshot).
|
|
883
905
|
// Do NOT strip when expression is user-driven: $route(, $search, $query. Those stay so Alpine can update.
|
|
884
906
|
// Same rule for :attr in stripPrerenderDynamicBindings: bindings with $x are kept (content stays for SEO).
|
|
@@ -888,9 +910,13 @@ function stripPrerenderedXDataDirectives(html) {
|
|
|
888
910
|
if (expr.includes('$search') || expr.includes('$query')) return false;
|
|
889
911
|
return true;
|
|
890
912
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
913
|
+
return html.replace(/<(\w+)([^>]*)>/g, (full, tag, attrs) => {
|
|
914
|
+
if (isHydrateMarkedAttrs(attrs)) return full;
|
|
915
|
+
let outAttrs = attrs;
|
|
916
|
+
outAttrs = outAttrs.replace(/\s+x-text="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
|
|
917
|
+
outAttrs = outAttrs.replace(/\s+x-html="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
|
|
918
|
+
return `<${tag}${outAttrs}>`;
|
|
919
|
+
});
|
|
894
920
|
}
|
|
895
921
|
|
|
896
922
|
// --- Don't bake Alpine-only state into the snapshot; only $x-driven content should be prerendered.
|
|
@@ -903,6 +929,7 @@ function stripPrerenderedXDataDirectives(html) {
|
|
|
903
929
|
function stripPrerenderDynamicBindings(html) {
|
|
904
930
|
return html.replace(/<(\w+)([^>]*)>/g, (match, tagName, attrsStr) => {
|
|
905
931
|
if (tagName.toLowerCase() === 'script') return match;
|
|
932
|
+
if (isHydrateMarkedAttrs(attrsStr)) return match;
|
|
906
933
|
const isAnchor = tagName.toLowerCase() === 'a';
|
|
907
934
|
const isImg = tagName.toLowerCase() === 'img';
|
|
908
935
|
let workAttrs = attrsStr;
|
|
@@ -942,6 +969,7 @@ function stripPrerenderDynamicBindings(html) {
|
|
|
942
969
|
// Drop :src / x-bind:src when img already has a baked src= (x-for / iterator expressions break hydrate).
|
|
943
970
|
function stripRedundantImgSrcBindings(html) {
|
|
944
971
|
return html.replace(/<img\b([^>]*)>/gi, (full, attrs) => {
|
|
972
|
+
if (isHydrateMarkedAttrs(attrs)) return full;
|
|
945
973
|
const srcM = attrs.match(/\ssrc=(["'])([\s\S]*?)\1/i);
|
|
946
974
|
if (!srcM || !String(srcM[2] || '').trim()) return full;
|
|
947
975
|
if (!/\s:src\s*=|\sx-bind:src\s*=/i.test(attrs)) return full;
|
|
@@ -960,6 +988,7 @@ function stripRedundantImgSrcBindings(html) {
|
|
|
960
988
|
// loop/item expressions while the attribute remains for CSS (e.g. inline layout that keys off [x-icon]).
|
|
961
989
|
function stripResolvedXIconDirectives(html) {
|
|
962
990
|
return html.replace(/<i\b([^>]*)>([\s\S]*?)<\/i>/gi, (full, attrs, inner) => {
|
|
991
|
+
if (isHydrateMarkedAttrs(attrs)) return full;
|
|
963
992
|
if (!/\sx-icon\s*=/i.test(attrs)) return full;
|
|
964
993
|
if (!/<svg\b/i.test(inner) || !/\bdata-icon\s*=/i.test(inner)) return full;
|
|
965
994
|
const cleaned = attrs
|
|
@@ -971,6 +1000,10 @@ function stripResolvedXIconDirectives(html) {
|
|
|
971
1000
|
});
|
|
972
1001
|
}
|
|
973
1002
|
|
|
1003
|
+
function stripPrerenderHydrateMarkers(html) {
|
|
1004
|
+
return html.replace(/\sdata-prerender-hydrate(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
974
1007
|
function markPrerenderedManifestComponents(html) {
|
|
975
1008
|
return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
|
|
976
1009
|
const a = attrs || '';
|
|
@@ -1007,6 +1040,10 @@ function stripEmptyInlineMaskStyles(html) {
|
|
|
1007
1040
|
// All project assets are copied into output, so root-relative paths become relative within output.
|
|
1008
1041
|
// Do NOT rewrite href on <a> tags (navigation links); only rewrite link/script/img so router gets clean paths.
|
|
1009
1042
|
|
|
1043
|
+
function isPrerenderBundleAssetPath(pathAfterSlash) {
|
|
1044
|
+
return /(^|\/)prerender\.(tailwind|utilities)\.css$/.test(pathAfterSlash);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1010
1047
|
function rewriteHtmlAssetPaths(html, depthWithinOutput) {
|
|
1011
1048
|
const prefix = depthWithinOutput > 0 ? '../'.repeat(depthWithinOutput) : '';
|
|
1012
1049
|
if (!prefix) return html;
|
|
@@ -1016,9 +1053,10 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
|
|
|
1016
1053
|
const tag = htmlBeforeMatch.slice(lastOpen + 1).match(/^(\w+)/);
|
|
1017
1054
|
return tag && tag[1].toLowerCase() === 'a';
|
|
1018
1055
|
}
|
|
1019
|
-
let out = html.replace(/(\s(href|src)=["'])\/(?!\/)/g, (match, lead,
|
|
1056
|
+
let out = html.replace(/(\s(href|src)=["'])\/(?!\/)([^'"]*)/g, (match, lead, _attr, rest, offset, fullString) => {
|
|
1020
1057
|
if (isAnchorTag(fullString.slice(0, offset))) return match;
|
|
1021
|
-
|
|
1058
|
+
if (isPrerenderBundleAssetPath(rest)) return match;
|
|
1059
|
+
return lead + prefix + rest;
|
|
1022
1060
|
});
|
|
1023
1061
|
out = out.replace(/(\s(href|src)=["'])(\.\.\/)+/g, (match, lead, attr, dots, offset, fullString) => {
|
|
1024
1062
|
if (isAnchorTag(fullString.slice(0, offset))) return match;
|
|
@@ -1027,6 +1065,17 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
|
|
|
1027
1065
|
return out;
|
|
1028
1066
|
}
|
|
1029
1067
|
|
|
1068
|
+
// Alpine x-data drives radio state; baked checked="" from the live DOM (e.g. yearly) fights monthly defaults.
|
|
1069
|
+
function stripPrerenderBakedRadioCheckedForXModel(html) {
|
|
1070
|
+
return html.replace(/<input\b([^>]*)>/gi, (full, attrs) => {
|
|
1071
|
+
if (!/\btype\s*=\s*["']radio["']/i.test(attrs)) return full;
|
|
1072
|
+
if (!/\bx-model\s*=/i.test(attrs)) return full;
|
|
1073
|
+
const next = attrs.replace(/\s+checked(?:\s*=\s*["'][^"']*["']|\s*=\s*[^\s>]+)?/gi, '');
|
|
1074
|
+
if (next === attrs) return full;
|
|
1075
|
+
return `<input${next}>`;
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1030
1079
|
// --- Canonical and hreflang (per-page injection) ---
|
|
1031
1080
|
|
|
1032
1081
|
function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
|
|
@@ -1761,15 +1810,27 @@ async function runPrerender(config) {
|
|
|
1761
1810
|
});
|
|
1762
1811
|
});
|
|
1763
1812
|
|
|
1813
|
+
// Mark data-hydrate islands so static compile transforms skip them.
|
|
1814
|
+
await page.evaluate(() => {
|
|
1815
|
+
document.querySelectorAll('[data-hydrate]').forEach((root) => {
|
|
1816
|
+
root.setAttribute('data-prerender-hydrate', '1');
|
|
1817
|
+
root.querySelectorAll('*').forEach((el) => el.setAttribute('data-prerender-hydrate', '1'));
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1764
1821
|
// x-for lists: keep static lists in the HTML for SEO; collapse only dynamic lists so Alpine re-renders.
|
|
1765
|
-
// Explicit: data-
|
|
1822
|
+
// Explicit: data-prerender="dynamic"|"skip". Inferred: x-for uses $search/$query,
|
|
1766
1823
|
// $url, $auth, or iterates over getter names (filtered*, results, searchResults). See docs prerender + local.data.
|
|
1767
1824
|
await page.evaluate(() => {
|
|
1768
1825
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
1826
|
+
if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) {
|
|
1827
|
+
tpl.removeAttribute('data-prerender-collapsed');
|
|
1828
|
+
tpl.removeAttribute('data-prerender-static-generated');
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1769
1831
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1770
1832
|
const prerender = (tpl.getAttribute('data-prerender') || '').toLowerCase();
|
|
1771
|
-
const
|
|
1772
|
-
const explicit = hasDataDynamic || prerender === 'dynamic' || prerender === 'skip';
|
|
1833
|
+
const explicit = prerender === 'dynamic' || prerender === 'skip';
|
|
1773
1834
|
const inferred = xFor.includes('$search') || xFor.includes('$query') ||
|
|
1774
1835
|
xFor.includes('$url') || xFor.includes('$auth') ||
|
|
1775
1836
|
/\bin\s+(filtered\w*|results|searchResults)\b/.test(xFor);
|
|
@@ -1883,6 +1944,7 @@ async function runPrerender(config) {
|
|
|
1883
1944
|
};
|
|
1884
1945
|
|
|
1885
1946
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
1947
|
+
if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
|
|
1886
1948
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1887
1949
|
const m = xFor.match(loopVarRegex);
|
|
1888
1950
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -1911,6 +1973,7 @@ async function runPrerender(config) {
|
|
|
1911
1973
|
const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
|
|
1912
1974
|
runBatch(() => {
|
|
1913
1975
|
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
1976
|
+
if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
|
|
1914
1977
|
const parent = tpl.parentNode;
|
|
1915
1978
|
if (!parent) {
|
|
1916
1979
|
tpl.remove();
|
|
@@ -1983,15 +2046,6 @@ async function runPrerender(config) {
|
|
|
1983
2046
|
});
|
|
1984
2047
|
});
|
|
1985
2048
|
|
|
1986
|
-
// Remove elements marked data-dynamic (so they are not in static HTML; client will render them).
|
|
1987
|
-
// Skip <template> since we only collapse those above; other elements and their subtree are removed.
|
|
1988
|
-
await page.evaluate(() => {
|
|
1989
|
-
const toRemove = Array.from(document.querySelectorAll('[data-dynamic]')).filter((el) => el.tagName !== 'TEMPLATE');
|
|
1990
|
-
const depth = (el) => { let d = 0; let n = el; while (n && n !== document.body) { d++; n = n.parentElement; } return d; };
|
|
1991
|
-
toRemove.sort((a, b) => depth(a) - depth(b));
|
|
1992
|
-
toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
|
|
1993
|
-
});
|
|
1994
|
-
|
|
1995
2049
|
const visibilityNormalizedPath = logicalPathToVisibilityNormalizedPath(pathSeg, locales);
|
|
1996
2050
|
await page.evaluate((np) => {
|
|
1997
2051
|
try {
|
|
@@ -2032,7 +2086,10 @@ async function runPrerender(config) {
|
|
|
2032
2086
|
for (const b of extracted.blocks) utilityBlocks.push(b);
|
|
2033
2087
|
}
|
|
2034
2088
|
if (tailwindBuilt) {
|
|
2035
|
-
html = injectBeforeHeadClose(
|
|
2089
|
+
html = injectBeforeHeadClose(
|
|
2090
|
+
html,
|
|
2091
|
+
`<link rel="stylesheet" href="${buildRootAssetPath(routerBasePath, 'prerender.tailwind.css')}">`
|
|
2092
|
+
);
|
|
2036
2093
|
}
|
|
2037
2094
|
html = stripDuplicatedLoopDirectives(html);
|
|
2038
2095
|
html = stripPrerenderedXDataDirectives(html);
|
|
@@ -2040,9 +2097,11 @@ async function runPrerender(config) {
|
|
|
2040
2097
|
const xData = { manifest, content };
|
|
2041
2098
|
html = resolveHeadXBindings(html, xData);
|
|
2042
2099
|
html = stripPrerenderDynamicBindings(html);
|
|
2100
|
+
html = stripPrerenderBakedRadioCheckedForXModel(html);
|
|
2043
2101
|
html = stripRedundantImgSrcBindings(html);
|
|
2044
2102
|
html = stripEmptyInlineMaskStyles(html);
|
|
2045
2103
|
html = stripResolvedXIconDirectives(html);
|
|
2104
|
+
html = stripPrerenderHydrateMarkers(html);
|
|
2046
2105
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
2047
2106
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
2048
2107
|
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
|
|
@@ -2050,9 +2109,17 @@ async function runPrerender(config) {
|
|
|
2050
2109
|
const injectOgLocale = ogLocale && hasOtherOgMeta(html);
|
|
2051
2110
|
if (injectOgLocale) html = stripOgLocaleFromHead(html);
|
|
2052
2111
|
const baseMeta = routerBasePath !== null ? `<meta name="manifest:router-base" content="${String(routerBasePath).replace(/"/g, '"')}">\n` : '';
|
|
2112
|
+
const routeEx = config.localeRouteExclude || [];
|
|
2113
|
+
const routeMeta =
|
|
2114
|
+
routeEx.length > 0
|
|
2115
|
+
? `<meta name="manifest:locale-route-exclude" content="${JSON.stringify(routeEx).replace(/"/g, '"')}">\n`
|
|
2116
|
+
: '';
|
|
2053
2117
|
const routeDepth = fileSegments.length;
|
|
2054
2118
|
const prerenderedMeta = `<meta name="manifest:prerendered" content="1">\n`;
|
|
2055
|
-
html = html.replace(
|
|
2119
|
+
html = html.replace(
|
|
2120
|
+
'</head>',
|
|
2121
|
+
`${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
|
|
2122
|
+
);
|
|
2056
2123
|
html = markPrerenderedManifestComponents(html);
|
|
2057
2124
|
mkdirSync(outDir, { recursive: true });
|
|
2058
2125
|
writeFileSync(outFile, html, 'utf8');
|
|
@@ -2113,7 +2180,7 @@ async function runPrerender(config) {
|
|
|
2113
2180
|
if (utilMerged.trim()) {
|
|
2114
2181
|
writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
|
|
2115
2182
|
process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
|
|
2116
|
-
postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css');
|
|
2183
|
+
postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
|
|
2117
2184
|
}
|
|
2118
2185
|
}
|
|
2119
2186
|
|