mnfst-render 0.2.9 → 0.3.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 +130 -61
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -76,6 +76,25 @@ async function flushAlpineEffects(page) {
|
|
|
76
76
|
.catch(() => {});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Same logical path → normalizedPath as waitForManifestPrerenderPipeline and
|
|
81
|
+
* manifest.router.visibility initialize (matchesCondition first argument).
|
|
82
|
+
*/
|
|
83
|
+
function logicalPathToVisibilityNormalizedPath(pathSeg, locales) {
|
|
84
|
+
const pathname = pathSeg ? `/${pathSeg}` : '/';
|
|
85
|
+
const clean = String(pathname || '/').replace(/^\/+|\/+$/g, '');
|
|
86
|
+
const parts = clean ? clean.split('/') : [];
|
|
87
|
+
const localeList = Array.isArray(locales) ? locales : [];
|
|
88
|
+
const logical =
|
|
89
|
+
parts.length > 0 && localeList.includes(parts[0])
|
|
90
|
+
? `/${parts.slice(1).join('/')}`
|
|
91
|
+
: clean
|
|
92
|
+
? `/${clean}`
|
|
93
|
+
: '/';
|
|
94
|
+
const to = logical === '//' ? '/' : logical;
|
|
95
|
+
return typeof to === 'string' && to !== '/' ? to.replace(/^\/|\/$/g, '') : '/';
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
/**
|
|
80
99
|
* After locale + route sync: run component swapping explicitly, then wait until Alpine data stores
|
|
81
100
|
* are settled. We call ManifestComponentsSwapping.processAll() directly because swapping only
|
|
@@ -1645,6 +1664,8 @@ async function runPrerender(config) {
|
|
|
1645
1664
|
return {
|
|
1646
1665
|
templateCount: templates.length,
|
|
1647
1666
|
nonCollapsedTemplateCount: templates.filter((t) => t.getAttribute('data-prerender-collapsed') !== '1').length,
|
|
1667
|
+
hint:
|
|
1668
|
+
'entries.staticGenerated is read before the x-for mark pass and is always false; use stage post-xfor-mark for data-prerender-static-generated.',
|
|
1648
1669
|
entries,
|
|
1649
1670
|
listDiagnostics,
|
|
1650
1671
|
};
|
|
@@ -1739,37 +1760,69 @@ async function runPrerender(config) {
|
|
|
1739
1760
|
});
|
|
1740
1761
|
});
|
|
1741
1762
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1763
|
+
if (config.debugPrerender) {
|
|
1764
|
+
const afterMark = await page.evaluate(() => {
|
|
1765
|
+
const rows = [];
|
|
1766
|
+
for (const tpl of document.querySelectorAll('template[x-for]')) {
|
|
1767
|
+
rows.push({
|
|
1768
|
+
xFor: (tpl.getAttribute('x-for') || '').slice(0, 140),
|
|
1769
|
+
collapsed: tpl.getAttribute('data-prerender-collapsed') === '1',
|
|
1770
|
+
staticGenerated: tpl.getAttribute('data-prerender-static-generated') === '1',
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
return {
|
|
1774
|
+
templateCount: rows.length,
|
|
1775
|
+
staticMarkedCount: rows.filter((r) => r.staticGenerated).length,
|
|
1776
|
+
collapsedCount: rows.filter((r) => r.collapsed).length,
|
|
1777
|
+
entries: rows.slice(0, 60),
|
|
1778
|
+
};
|
|
1779
|
+
}).catch(() => null);
|
|
1780
|
+
pushDebug({ path: displayPath, stage: 'post-xfor-mark', metrics: afterMark });
|
|
1781
|
+
}
|
|
1749
1782
|
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1783
|
+
// Strip loop-scope bindings from x-for clones while <template> nodes still exist.
|
|
1784
|
+
// (If we remove static templates first, querySelectorAll('template[x-for]') misses them and clones
|
|
1785
|
+
// keep x-text/x-bind referencing card/item — Alpine then mutates or errors on the static HTML.)
|
|
1752
1786
|
await page.evaluate(() => {
|
|
1753
1787
|
const loopVarRegex = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
1754
1788
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-on:|@)/;
|
|
1755
1789
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
1756
|
-
const
|
|
1757
|
-
if (!el) return false;
|
|
1790
|
+
const stripLoopBindings = (el, itemVar, indexVar) => {
|
|
1758
1791
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1759
1792
|
for (const node of nodes) {
|
|
1760
1793
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
1761
1794
|
for (const attr of attrs) {
|
|
1762
1795
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
1763
1796
|
const expr = attr.value || '';
|
|
1764
|
-
if (hasVar(expr, itemVar) || hasVar(expr, indexVar))
|
|
1797
|
+
if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) {
|
|
1798
|
+
const name = attr.name;
|
|
1799
|
+
if (name === 'x-text' || name === 'x-html') {
|
|
1800
|
+
if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
|
|
1801
|
+
node.removeAttribute(name);
|
|
1802
|
+
}
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (name === 'x-show' || name === 'x-if') {
|
|
1806
|
+
node.removeAttribute(name);
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
let boundAttr = '';
|
|
1810
|
+
if (name.startsWith(':')) boundAttr = name.slice(1);
|
|
1811
|
+
else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
|
|
1812
|
+
if (boundAttr) {
|
|
1813
|
+
const concrete = node.getAttribute(boundAttr);
|
|
1814
|
+
if (concrete != null && String(concrete).trim() !== '') {
|
|
1815
|
+
node.removeAttribute(name);
|
|
1816
|
+
}
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
node.removeAttribute(name);
|
|
1820
|
+
}
|
|
1765
1821
|
}
|
|
1766
1822
|
}
|
|
1767
|
-
return false;
|
|
1768
1823
|
};
|
|
1769
1824
|
|
|
1770
|
-
|
|
1771
|
-
// Running this on all x-for templates can remove valid prerendered list items.
|
|
1772
|
-
document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
|
|
1825
|
+
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
1773
1826
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1774
1827
|
const m = xFor.match(loopVarRegex);
|
|
1775
1828
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -1782,68 +1835,69 @@ async function runPrerender(config) {
|
|
|
1782
1835
|
|
|
1783
1836
|
let next = tpl.nextElementSibling;
|
|
1784
1837
|
while (next) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const referencesLoopScope = elementReferencesLoopScope(next, itemVar, indexVar);
|
|
1789
|
-
|
|
1790
|
-
const toRemove = next;
|
|
1838
|
+
if (next.tagName !== tag) break;
|
|
1839
|
+
stripLoopBindings(next, itemVar, indexVar);
|
|
1791
1840
|
next = next.nextElementSibling;
|
|
1792
|
-
if (referencesLoopScope) toRemove.remove();
|
|
1793
|
-
else break;
|
|
1794
1841
|
}
|
|
1795
1842
|
});
|
|
1796
1843
|
});
|
|
1797
1844
|
|
|
1798
|
-
//
|
|
1799
|
-
//
|
|
1845
|
+
// Remove static x-for templates once static clones are generated.
|
|
1846
|
+
// Alpine registers a cleanup on <template x-for> that removes every node in _x_lookup when the
|
|
1847
|
+
// template is detached — so tpl.remove() alone deletes all sibling clones (empty grids in output).
|
|
1848
|
+
// Replace each clone with a deep cloneNode first so teardown targets detached nodes; copies stay in DOM.
|
|
1849
|
+
await page.evaluate(() => {
|
|
1850
|
+
const A = window.Alpine;
|
|
1851
|
+
const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
|
|
1852
|
+
runBatch(() => {
|
|
1853
|
+
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
1854
|
+
const parent = tpl.parentNode;
|
|
1855
|
+
if (!parent) {
|
|
1856
|
+
tpl.remove();
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const first = tpl.content?.firstElementChild;
|
|
1860
|
+
if (!first) {
|
|
1861
|
+
tpl.remove();
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const tag = first.tagName;
|
|
1865
|
+
const cls = first.getAttribute('class') || '';
|
|
1866
|
+
let n = tpl.nextElementSibling;
|
|
1867
|
+
while (n && n.tagName === tag) {
|
|
1868
|
+
if ((n.getAttribute('class') || '') !== cls) break;
|
|
1869
|
+
const next = n.nextElementSibling;
|
|
1870
|
+
n.replaceWith(n.cloneNode(true));
|
|
1871
|
+
n = next;
|
|
1872
|
+
}
|
|
1873
|
+
tpl.remove();
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
// Remove orphan x-for clones that still reference loop-scope vars (e.g. image/index)
|
|
1879
|
+
// outside their template scope. These throw Alpine errors in live static hosting.
|
|
1800
1880
|
await page.evaluate(() => {
|
|
1801
1881
|
const loopVarRegex = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
1802
1882
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-on:|@)/;
|
|
1803
1883
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
1804
|
-
const
|
|
1884
|
+
const elementReferencesLoopScope = (el, itemVar, indexVar) => {
|
|
1885
|
+
if (!el) return false;
|
|
1805
1886
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1806
1887
|
for (const node of nodes) {
|
|
1807
1888
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
1808
1889
|
for (const attr of attrs) {
|
|
1809
1890
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
1810
1891
|
const expr = attr.value || '';
|
|
1811
|
-
if (hasVar(expr, itemVar) || hasVar(expr, indexVar))
|
|
1812
|
-
const name = attr.name;
|
|
1813
|
-
// Remove text/html bindings only when static content already exists.
|
|
1814
|
-
if (name === 'x-text' || name === 'x-html') {
|
|
1815
|
-
if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
|
|
1816
|
-
node.removeAttribute(name);
|
|
1817
|
-
}
|
|
1818
|
-
continue;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// Remove x-show/x-if if they reference loop vars; cloned node is now static.
|
|
1822
|
-
if (name === 'x-show' || name === 'x-if') {
|
|
1823
|
-
node.removeAttribute(name);
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
// For :attr / x-bind:attr, only remove binding if a concrete attr is present.
|
|
1828
|
-
let boundAttr = '';
|
|
1829
|
-
if (name.startsWith(':')) boundAttr = name.slice(1);
|
|
1830
|
-
else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
|
|
1831
|
-
if (boundAttr) {
|
|
1832
|
-
const concrete = node.getAttribute(boundAttr);
|
|
1833
|
-
if (concrete != null && String(concrete).trim() !== '') {
|
|
1834
|
-
node.removeAttribute(name);
|
|
1835
|
-
}
|
|
1836
|
-
continue;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
// Event/other loop-scoped bindings are unsafe on static clones.
|
|
1840
|
-
node.removeAttribute(name);
|
|
1841
|
-
}
|
|
1892
|
+
if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) return true;
|
|
1842
1893
|
}
|
|
1843
1894
|
}
|
|
1895
|
+
return false;
|
|
1844
1896
|
};
|
|
1845
1897
|
|
|
1846
|
-
|
|
1898
|
+
// Only clean up templates we intentionally collapsed above.
|
|
1899
|
+
// Running this on all x-for templates can remove valid prerendered list items.
|
|
1900
|
+
document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
|
|
1847
1901
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1848
1902
|
const m = xFor.match(loopVarRegex);
|
|
1849
1903
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -1856,9 +1910,15 @@ async function runPrerender(config) {
|
|
|
1856
1910
|
|
|
1857
1911
|
let next = tpl.nextElementSibling;
|
|
1858
1912
|
while (next) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1913
|
+
const sameTag = next.tagName === tag;
|
|
1914
|
+
if (!sameTag) break;
|
|
1915
|
+
|
|
1916
|
+
const referencesLoopScope = elementReferencesLoopScope(next, itemVar, indexVar);
|
|
1917
|
+
|
|
1918
|
+
const toRemove = next;
|
|
1861
1919
|
next = next.nextElementSibling;
|
|
1920
|
+
if (referencesLoopScope) toRemove.remove();
|
|
1921
|
+
else break;
|
|
1862
1922
|
}
|
|
1863
1923
|
});
|
|
1864
1924
|
});
|
|
@@ -1872,6 +1932,15 @@ async function runPrerender(config) {
|
|
|
1872
1932
|
toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
|
|
1873
1933
|
});
|
|
1874
1934
|
|
|
1935
|
+
const visibilityNormalizedPath = logicalPathToVisibilityNormalizedPath(pathSeg, locales);
|
|
1936
|
+
await page.evaluate((np) => {
|
|
1937
|
+
try {
|
|
1938
|
+
window.ManifestRoutingVisibility?.processRouteVisibility?.(np);
|
|
1939
|
+
} catch {
|
|
1940
|
+
/* no-op */
|
|
1941
|
+
}
|
|
1942
|
+
}, visibilityNormalizedPath);
|
|
1943
|
+
|
|
1875
1944
|
// Remove route-hidden content ([x-route] with inline style display:none) so each prerendered page contains only that route's HTML.
|
|
1876
1945
|
await page.evaluate(() => {
|
|
1877
1946
|
const reDisplayNone = /\bdisplay\s*:\s*none\b/i;
|