mnfst-render 0.2.8 → 0.3.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 +170 -172
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -77,133 +77,128 @@ async function flushAlpineEffects(page) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
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
|
+
|
|
98
|
+
/**
|
|
99
|
+
* After locale + route sync: run component swapping explicitly, then wait until Alpine data stores
|
|
100
|
+
* are settled. We call ManifestComponentsSwapping.processAll() directly because swapping only
|
|
101
|
+
* subscribes to manifest:route-change if window.ManifestRouting already exists at initialize() time;
|
|
102
|
+
* when the router loads after components, that listener is never registered and no second
|
|
103
|
+
* manifest:components-processed fires.
|
|
83
104
|
*/
|
|
84
105
|
async function waitForManifestPrerenderPipeline(page, { allLocales, currentLocale, timeoutMs }) {
|
|
85
106
|
const result = await page
|
|
86
107
|
.evaluate(
|
|
87
|
-
({ localeList, loc, ms }) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
async ({ localeList, loc, ms }) => {
|
|
109
|
+
const sleep = (n) => new Promise((r) => setTimeout(r, n));
|
|
110
|
+
const deadline = Date.now() + ms;
|
|
111
|
+
|
|
112
|
+
const checkData = () => {
|
|
113
|
+
try {
|
|
114
|
+
const Alpine = window.Alpine;
|
|
115
|
+
if (!Alpine?.store) return true;
|
|
116
|
+
const d = Alpine.store('data');
|
|
117
|
+
if (!d) return true;
|
|
118
|
+
if (d._localeChanging) return false;
|
|
119
|
+
for (const k of Object.keys(d)) {
|
|
120
|
+
if (!k.startsWith('_') || !k.endsWith('_state')) continue;
|
|
121
|
+
const s = d[k];
|
|
122
|
+
if (s && typeof s === 'object' && s.loading) return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
93
129
|
|
|
94
|
-
|
|
130
|
+
try {
|
|
131
|
+
const locales = Array.isArray(localeList) ? localeList : [];
|
|
132
|
+
if (loc && typeof loc === 'string') {
|
|
95
133
|
try {
|
|
96
|
-
|
|
97
|
-
if (!Alpine?.store) return true;
|
|
98
|
-
const d = Alpine.store('data');
|
|
99
|
-
if (!d) return true;
|
|
100
|
-
if (d._localeChanging) return false;
|
|
101
|
-
for (const k of Object.keys(d)) {
|
|
102
|
-
if (!k.startsWith('_') || !k.endsWith('_state')) continue;
|
|
103
|
-
const s = d[k];
|
|
104
|
-
if (s && typeof s === 'object' && s.loading) return false;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
134
|
+
document.documentElement.lang = loc;
|
|
107
135
|
} catch {
|
|
108
|
-
|
|
136
|
+
/* no-op */
|
|
109
137
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
window.removeEventListener('manifest:components-processed', onComponents);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const tryFinish = () => {
|
|
121
|
-
if (!dispatched) return;
|
|
122
|
-
if (skipComponentsWait()) {
|
|
123
|
-
sawComponents = true;
|
|
138
|
+
}
|
|
139
|
+
const store = typeof Alpine !== 'undefined' && Alpine.store ? Alpine.store('locale') : null;
|
|
140
|
+
if (store) {
|
|
141
|
+
if (!Array.isArray(store.available) || store.available.length === 0) {
|
|
142
|
+
store.available = locales.slice();
|
|
143
|
+
} else {
|
|
144
|
+
store.available = Array.from(new Set([...store.available, ...locales]));
|
|
124
145
|
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
resolve({ ok: true });
|
|
146
|
+
if (loc && typeof loc === 'string') {
|
|
147
|
+
store.current = loc;
|
|
128
148
|
}
|
|
129
|
-
}
|
|
149
|
+
}
|
|
130
150
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
sawComponents = true;
|
|
134
|
-
tryFinish();
|
|
151
|
+
if (loc && typeof loc === 'string') {
|
|
152
|
+
window.dispatchEvent(new CustomEvent('localechange', { detail: { locale: loc } }));
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
window.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
const rawRoute = window.ManifestRoutingNavigation?.getCurrentRoute?.() ?? window.location.pathname;
|
|
156
|
+
const clean = String(rawRoute || '/').replace(/^\/+|\/+$/g, '');
|
|
157
|
+
const parts = clean ? clean.split('/') : [];
|
|
158
|
+
const logical =
|
|
159
|
+
parts.length > 0 && locales.includes(parts[0])
|
|
160
|
+
? '/' + parts.slice(1).join('/')
|
|
161
|
+
: clean
|
|
162
|
+
? '/' + clean
|
|
163
|
+
: '/';
|
|
164
|
+
const to = logical === '//' ? '/' : logical;
|
|
165
|
+
const normalizedPath =
|
|
166
|
+
typeof to === 'string' && to !== '/' ? to.replace(/^\/|\/$/g, '') : '/';
|
|
167
|
+
|
|
168
|
+
window.dispatchEvent(
|
|
169
|
+
new CustomEvent('manifest:route-change', {
|
|
170
|
+
detail: {
|
|
171
|
+
from: to,
|
|
172
|
+
to,
|
|
173
|
+
normalizedPath,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch {
|
|
160
|
-
/* no-op */
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const store = typeof Alpine !== 'undefined' && Alpine.store ? Alpine.store('locale') : null;
|
|
164
|
-
if (store) {
|
|
165
|
-
if (!Array.isArray(store.available) || store.available.length === 0) {
|
|
166
|
-
store.available = locales.slice();
|
|
167
|
-
} else {
|
|
168
|
-
store.available = Array.from(new Set([...store.available, ...locales]));
|
|
169
|
-
}
|
|
170
|
-
if (loc && typeof loc === 'string') {
|
|
171
|
-
store.current = loc;
|
|
172
|
-
}
|
|
179
|
+
if (window.ManifestComponentsSwapping?.processAll) {
|
|
180
|
+
try {
|
|
181
|
+
await window.ManifestComponentsSwapping.processAll(normalizedPath);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return { ok: false, reason: 'processAll-error', message: String(e?.message || e) };
|
|
173
184
|
}
|
|
185
|
+
}
|
|
174
186
|
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
while (Date.now() < deadline) {
|
|
188
|
+
if (checkData()) {
|
|
189
|
+
return { ok: true };
|
|
177
190
|
}
|
|
178
|
-
|
|
179
|
-
const rawRoute = window.ManifestRoutingNavigation?.getCurrentRoute?.() ?? window.location.pathname;
|
|
180
|
-
const clean = String(rawRoute || '/').replace(/^\/+|\/+$/g, '');
|
|
181
|
-
const parts = clean ? clean.split('/') : [];
|
|
182
|
-
const logical =
|
|
183
|
-
parts.length > 0 && locales.includes(parts[0])
|
|
184
|
-
? '/' + parts.slice(1).join('/')
|
|
185
|
-
: clean
|
|
186
|
-
? '/' + clean
|
|
187
|
-
: '/';
|
|
188
|
-
const to = logical === '//' ? '/' : logical;
|
|
189
|
-
|
|
190
|
-
dispatched = true;
|
|
191
|
-
window.dispatchEvent(
|
|
192
|
-
new CustomEvent('manifest:route-change', {
|
|
193
|
-
detail: {
|
|
194
|
-
from: to,
|
|
195
|
-
to,
|
|
196
|
-
normalizedPath: typeof to === 'string' && to !== '/' ? to.replace(/^\/|\/$/g, '') : '/',
|
|
197
|
-
},
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
201
|
-
tryFinish();
|
|
202
|
-
} catch (err) {
|
|
203
|
-
cleanup();
|
|
204
|
-
resolve({ ok: false, reason: 'error', message: String(err?.message || err) });
|
|
191
|
+
await sleep(50);
|
|
205
192
|
}
|
|
206
|
-
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
reason: 'timeout',
|
|
196
|
+
dataOk: checkData(),
|
|
197
|
+
hadSwapping: !!window.ManifestComponentsSwapping?.processAll,
|
|
198
|
+
};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { ok: false, reason: 'error', message: String(err?.message || err) };
|
|
201
|
+
}
|
|
207
202
|
},
|
|
208
203
|
{
|
|
209
204
|
localeList: allLocales,
|
|
@@ -216,7 +211,7 @@ async function waitForManifestPrerenderPipeline(page, { allLocales, currentLocal
|
|
|
216
211
|
if (!result?.ok) {
|
|
217
212
|
const parts = [`prerender: pipeline wait incomplete (${result?.reason ?? 'unknown'})`];
|
|
218
213
|
if (result?.dataOk === false) parts.push('data still loading');
|
|
219
|
-
if (result?.
|
|
214
|
+
if (result?.message) parts.push(result.message);
|
|
220
215
|
process.stdout.write(`${parts.join('; ')}\n`);
|
|
221
216
|
}
|
|
222
217
|
}
|
|
@@ -1763,37 +1758,49 @@ async function runPrerender(config) {
|
|
|
1763
1758
|
});
|
|
1764
1759
|
});
|
|
1765
1760
|
|
|
1766
|
-
//
|
|
1767
|
-
//
|
|
1768
|
-
|
|
1769
|
-
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
1770
|
-
tpl.remove();
|
|
1771
|
-
});
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
// Remove orphan x-for clones that still reference loop-scope vars (e.g. image/index)
|
|
1775
|
-
// outside their template scope. These throw Alpine errors in live static hosting.
|
|
1761
|
+
// Strip loop-scope bindings from x-for clones while <template> nodes still exist.
|
|
1762
|
+
// (If we remove static templates first, querySelectorAll('template[x-for]') misses them and clones
|
|
1763
|
+
// keep x-text/x-bind referencing card/item — Alpine then mutates or errors on the static HTML.)
|
|
1776
1764
|
await page.evaluate(() => {
|
|
1777
1765
|
const loopVarRegex = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
1778
1766
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-on:|@)/;
|
|
1779
1767
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
1780
|
-
const
|
|
1781
|
-
if (!el) return false;
|
|
1768
|
+
const stripLoopBindings = (el, itemVar, indexVar) => {
|
|
1782
1769
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1783
1770
|
for (const node of nodes) {
|
|
1784
1771
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
1785
1772
|
for (const attr of attrs) {
|
|
1786
1773
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
1787
1774
|
const expr = attr.value || '';
|
|
1788
|
-
if (hasVar(expr, itemVar) || hasVar(expr, indexVar))
|
|
1775
|
+
if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) {
|
|
1776
|
+
const name = attr.name;
|
|
1777
|
+
if (name === 'x-text' || name === 'x-html') {
|
|
1778
|
+
if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
|
|
1779
|
+
node.removeAttribute(name);
|
|
1780
|
+
}
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
if (name === 'x-show' || name === 'x-if') {
|
|
1784
|
+
node.removeAttribute(name);
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
let boundAttr = '';
|
|
1788
|
+
if (name.startsWith(':')) boundAttr = name.slice(1);
|
|
1789
|
+
else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
|
|
1790
|
+
if (boundAttr) {
|
|
1791
|
+
const concrete = node.getAttribute(boundAttr);
|
|
1792
|
+
if (concrete != null && String(concrete).trim() !== '') {
|
|
1793
|
+
node.removeAttribute(name);
|
|
1794
|
+
}
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
node.removeAttribute(name);
|
|
1798
|
+
}
|
|
1789
1799
|
}
|
|
1790
1800
|
}
|
|
1791
|
-
return false;
|
|
1792
1801
|
};
|
|
1793
1802
|
|
|
1794
|
-
|
|
1795
|
-
// Running this on all x-for templates can remove valid prerendered list items.
|
|
1796
|
-
document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
|
|
1803
|
+
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
1797
1804
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1798
1805
|
const m = xFor.match(loopVarRegex);
|
|
1799
1806
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -1806,68 +1813,44 @@ async function runPrerender(config) {
|
|
|
1806
1813
|
|
|
1807
1814
|
let next = tpl.nextElementSibling;
|
|
1808
1815
|
while (next) {
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const referencesLoopScope = elementReferencesLoopScope(next, itemVar, indexVar);
|
|
1813
|
-
|
|
1814
|
-
const toRemove = next;
|
|
1816
|
+
if (next.tagName !== tag) break;
|
|
1817
|
+
stripLoopBindings(next, itemVar, indexVar);
|
|
1815
1818
|
next = next.nextElementSibling;
|
|
1816
|
-
if (referencesLoopScope) toRemove.remove();
|
|
1817
|
-
else break;
|
|
1818
1819
|
}
|
|
1819
1820
|
});
|
|
1820
1821
|
});
|
|
1821
1822
|
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1823
|
+
// Remove static x-for templates once static clones are generated.
|
|
1824
|
+
// This prevents Alpine from rendering duplicate lists at runtime.
|
|
1825
|
+
await page.evaluate(() => {
|
|
1826
|
+
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
1827
|
+
tpl.remove();
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
// Remove orphan x-for clones that still reference loop-scope vars (e.g. image/index)
|
|
1832
|
+
// outside their template scope. These throw Alpine errors in live static hosting.
|
|
1824
1833
|
await page.evaluate(() => {
|
|
1825
1834
|
const loopVarRegex = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
1826
1835
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-on:|@)/;
|
|
1827
1836
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
1828
|
-
const
|
|
1837
|
+
const elementReferencesLoopScope = (el, itemVar, indexVar) => {
|
|
1838
|
+
if (!el) return false;
|
|
1829
1839
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1830
1840
|
for (const node of nodes) {
|
|
1831
1841
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
1832
1842
|
for (const attr of attrs) {
|
|
1833
1843
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
1834
1844
|
const expr = attr.value || '';
|
|
1835
|
-
if (hasVar(expr, itemVar) || hasVar(expr, indexVar))
|
|
1836
|
-
const name = attr.name;
|
|
1837
|
-
// Remove text/html bindings only when static content already exists.
|
|
1838
|
-
if (name === 'x-text' || name === 'x-html') {
|
|
1839
|
-
if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
|
|
1840
|
-
node.removeAttribute(name);
|
|
1841
|
-
}
|
|
1842
|
-
continue;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// Remove x-show/x-if if they reference loop vars; cloned node is now static.
|
|
1846
|
-
if (name === 'x-show' || name === 'x-if') {
|
|
1847
|
-
node.removeAttribute(name);
|
|
1848
|
-
continue;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// For :attr / x-bind:attr, only remove binding if a concrete attr is present.
|
|
1852
|
-
let boundAttr = '';
|
|
1853
|
-
if (name.startsWith(':')) boundAttr = name.slice(1);
|
|
1854
|
-
else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
|
|
1855
|
-
if (boundAttr) {
|
|
1856
|
-
const concrete = node.getAttribute(boundAttr);
|
|
1857
|
-
if (concrete != null && String(concrete).trim() !== '') {
|
|
1858
|
-
node.removeAttribute(name);
|
|
1859
|
-
}
|
|
1860
|
-
continue;
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Event/other loop-scoped bindings are unsafe on static clones.
|
|
1864
|
-
node.removeAttribute(name);
|
|
1865
|
-
}
|
|
1845
|
+
if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) return true;
|
|
1866
1846
|
}
|
|
1867
1847
|
}
|
|
1848
|
+
return false;
|
|
1868
1849
|
};
|
|
1869
1850
|
|
|
1870
|
-
|
|
1851
|
+
// Only clean up templates we intentionally collapsed above.
|
|
1852
|
+
// Running this on all x-for templates can remove valid prerendered list items.
|
|
1853
|
+
document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
|
|
1871
1854
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
1872
1855
|
const m = xFor.match(loopVarRegex);
|
|
1873
1856
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -1880,9 +1863,15 @@ async function runPrerender(config) {
|
|
|
1880
1863
|
|
|
1881
1864
|
let next = tpl.nextElementSibling;
|
|
1882
1865
|
while (next) {
|
|
1883
|
-
|
|
1884
|
-
|
|
1866
|
+
const sameTag = next.tagName === tag;
|
|
1867
|
+
if (!sameTag) break;
|
|
1868
|
+
|
|
1869
|
+
const referencesLoopScope = elementReferencesLoopScope(next, itemVar, indexVar);
|
|
1870
|
+
|
|
1871
|
+
const toRemove = next;
|
|
1885
1872
|
next = next.nextElementSibling;
|
|
1873
|
+
if (referencesLoopScope) toRemove.remove();
|
|
1874
|
+
else break;
|
|
1886
1875
|
}
|
|
1887
1876
|
});
|
|
1888
1877
|
});
|
|
@@ -1896,6 +1885,15 @@ async function runPrerender(config) {
|
|
|
1896
1885
|
toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
|
|
1897
1886
|
});
|
|
1898
1887
|
|
|
1888
|
+
const visibilityNormalizedPath = logicalPathToVisibilityNormalizedPath(pathSeg, locales);
|
|
1889
|
+
await page.evaluate((np) => {
|
|
1890
|
+
try {
|
|
1891
|
+
window.ManifestRoutingVisibility?.processRouteVisibility?.(np);
|
|
1892
|
+
} catch {
|
|
1893
|
+
/* no-op */
|
|
1894
|
+
}
|
|
1895
|
+
}, visibilityNormalizedPath);
|
|
1896
|
+
|
|
1899
1897
|
// Remove route-hidden content ([x-route] with inline style display:none) so each prerendered page contains only that route's HTML.
|
|
1900
1898
|
await page.evaluate(() => {
|
|
1901
1899
|
const reDisplayNone = /\bdisplay\s*:\s*none\b/i;
|