mnfst-render 0.2.6 → 0.2.8
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 +401 -34
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -23,6 +23,204 @@ async function importFromProject(moduleName) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* localechange reloads localized sources asynchronously; waitForNetworkIdle can finish before
|
|
28
|
+
* Alpine.store('data') updates. Without this, x-for / $modify('items') lists often snapshot empty.
|
|
29
|
+
*/
|
|
30
|
+
async function waitForAlpineDataStoresSettled(page, timeoutMs = 12000) {
|
|
31
|
+
const interval = 100;
|
|
32
|
+
const deadline = Date.now() + timeoutMs;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
const ok = await page
|
|
35
|
+
.evaluate(() => {
|
|
36
|
+
try {
|
|
37
|
+
const Alpine = window.Alpine;
|
|
38
|
+
if (!Alpine?.store) return true;
|
|
39
|
+
const d = Alpine.store('data');
|
|
40
|
+
if (!d) return true;
|
|
41
|
+
if (d._localeChanging) return false;
|
|
42
|
+
for (const k of Object.keys(d)) {
|
|
43
|
+
if (!k.startsWith('_') || !k.endsWith('_state')) continue;
|
|
44
|
+
const s = d[k];
|
|
45
|
+
if (s && typeof s === 'object' && s.loading) return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch(() => true);
|
|
53
|
+
if (ok) return;
|
|
54
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function flushAlpineEffects(page) {
|
|
59
|
+
await page
|
|
60
|
+
.evaluate(() => {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
try {
|
|
63
|
+
if (typeof Alpine !== 'undefined' && typeof Alpine.nextTick === 'function') {
|
|
64
|
+
Alpine.nextTick(() => {
|
|
65
|
+
if (typeof Alpine.nextTick === 'function') Alpine.nextTick(resolve);
|
|
66
|
+
else resolve();
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
queueMicrotask(resolve);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
resolve();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* After locale + route sync, wait until (1) Manifest fires manifest:components-processed for that pass
|
|
81
|
+
* (component swapping finished) and (2) no Alpine data _*_state is still loading / _localeChanging.
|
|
82
|
+
* Listener is registered before dispatch so the initial page load event is ignored.
|
|
83
|
+
*/
|
|
84
|
+
async function waitForManifestPrerenderPipeline(page, { allLocales, currentLocale, timeoutMs }) {
|
|
85
|
+
const result = await page
|
|
86
|
+
.evaluate(
|
|
87
|
+
({ localeList, loc, ms }) => {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const deadline = Date.now() + ms;
|
|
90
|
+
let dispatched = false;
|
|
91
|
+
let sawComponents = false;
|
|
92
|
+
let poll = 0;
|
|
93
|
+
|
|
94
|
+
const checkData = () => {
|
|
95
|
+
try {
|
|
96
|
+
const Alpine = window.Alpine;
|
|
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;
|
|
107
|
+
} catch {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const skipComponentsWait = () =>
|
|
113
|
+
!window.__manifestComponentsInitialized || !window.ManifestComponentsSwapping;
|
|
114
|
+
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
if (poll) clearInterval(poll);
|
|
117
|
+
window.removeEventListener('manifest:components-processed', onComponents);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const tryFinish = () => {
|
|
121
|
+
if (!dispatched) return;
|
|
122
|
+
if (skipComponentsWait()) {
|
|
123
|
+
sawComponents = true;
|
|
124
|
+
}
|
|
125
|
+
if (sawComponents && checkData()) {
|
|
126
|
+
cleanup();
|
|
127
|
+
resolve({ ok: true });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function onComponents() {
|
|
132
|
+
if (!dispatched) return;
|
|
133
|
+
sawComponents = true;
|
|
134
|
+
tryFinish();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
window.addEventListener('manifest:components-processed', onComponents);
|
|
138
|
+
|
|
139
|
+
poll = setInterval(() => {
|
|
140
|
+
if (Date.now() > deadline) {
|
|
141
|
+
cleanup();
|
|
142
|
+
resolve({
|
|
143
|
+
ok: false,
|
|
144
|
+
reason: 'timeout',
|
|
145
|
+
sawComponents,
|
|
146
|
+
dataOk: checkData(),
|
|
147
|
+
skippedComponents: skipComponentsWait(),
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
tryFinish();
|
|
152
|
+
}, 50);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const locales = Array.isArray(localeList) ? localeList : [];
|
|
156
|
+
if (loc && typeof loc === 'string') {
|
|
157
|
+
try {
|
|
158
|
+
document.documentElement.lang = loc;
|
|
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
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (loc && typeof loc === 'string') {
|
|
176
|
+
window.dispatchEvent(new CustomEvent('localechange', { detail: { locale: loc } }));
|
|
177
|
+
}
|
|
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) });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
localeList: allLocales,
|
|
210
|
+
loc: currentLocale,
|
|
211
|
+
ms: timeoutMs,
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
.catch((e) => ({ ok: false, reason: 'evaluate', message: String(e) }));
|
|
215
|
+
|
|
216
|
+
if (!result?.ok) {
|
|
217
|
+
const parts = [`prerender: pipeline wait incomplete (${result?.reason ?? 'unknown'})`];
|
|
218
|
+
if (result?.dataOk === false) parts.push('data still loading');
|
|
219
|
+
if (result?.sawComponents === false && !result?.skippedComponents) parts.push('no manifest:components-processed');
|
|
220
|
+
process.stdout.write(`${parts.join('; ')}\n`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
26
224
|
// --- Config ------------------------------------------------------------------
|
|
27
225
|
|
|
28
226
|
function parseArgs() {
|
|
@@ -39,6 +237,7 @@ function parseArgs() {
|
|
|
39
237
|
if (args[i] === '--wait-after-idle' && args[i + 1]) { out.waitAfterIdle = parseInt(args[++i], 10); continue; }
|
|
40
238
|
if (args[i] === '--concurrency' && args[i + 1]) { out.concurrency = parseInt(args[++i], 10); continue; }
|
|
41
239
|
if (args[i] === '--dry-run') { out.dryRun = true; continue; }
|
|
240
|
+
if (args[i] === '--debug-prerender') { out.debugPrerender = true; continue; }
|
|
42
241
|
}
|
|
43
242
|
return out;
|
|
44
243
|
}
|
|
@@ -86,6 +285,8 @@ function resolveConfig() {
|
|
|
86
285
|
waitAfterIdle: Math.max(0, cli.waitAfterIdle ?? pre.waitAfterIdle ?? 0),
|
|
87
286
|
concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? 6),
|
|
88
287
|
dryRun: !!cli.dryRun,
|
|
288
|
+
debugPrerender: !!(cli.debugPrerender ?? pre.debugPrerender),
|
|
289
|
+
pipelineTimeout: Math.max(3000, Number(pre.pipelineTimeout) || 25000),
|
|
89
290
|
};
|
|
90
291
|
}
|
|
91
292
|
|
|
@@ -729,6 +930,29 @@ function stripPrerenderDynamicBindings(html) {
|
|
|
729
930
|
});
|
|
730
931
|
}
|
|
731
932
|
|
|
933
|
+
// Remove empty inline mask-image styles emitted before data resolves
|
|
934
|
+
// (e.g. style="mask-image: url()"), while keeping any :style/x-bind:style bindings.
|
|
935
|
+
function stripEmptyInlineMaskStyles(html) {
|
|
936
|
+
return html.replace(/<(\w+)([^>]*)>/g, (full, tag, attrs) => {
|
|
937
|
+
const styleMatch = attrs.match(/\sstyle=(["'])([\s\S]*?)\1/i);
|
|
938
|
+
if (!styleMatch) return full;
|
|
939
|
+
const quote = styleMatch[1];
|
|
940
|
+
const rawStyle = styleMatch[2] || '';
|
|
941
|
+
const cleaned = rawStyle
|
|
942
|
+
.replace(/\bmask-image\s*:\s*url\(\s*(?:''|""|)\s*\)\s*;?/gi, '')
|
|
943
|
+
.replace(/\b-webkit-mask-image\s*:\s*url\(\s*(?:''|""|)\s*\)\s*;?/gi, '')
|
|
944
|
+
.trim()
|
|
945
|
+
.replace(/^\s*;\s*|\s*;\s*$/g, '');
|
|
946
|
+
|
|
947
|
+
if (!cleaned) {
|
|
948
|
+
const newAttrs = attrs.replace(/\sstyle=(["'])[\s\S]*?\1/i, '');
|
|
949
|
+
return `<${tag}${newAttrs}>`;
|
|
950
|
+
}
|
|
951
|
+
const rebuilt = attrs.replace(/\sstyle=(["'])[\s\S]*?\1/i, ` style=${quote}${cleaned}${quote}`);
|
|
952
|
+
return `<${tag}${rebuilt}>`;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
732
956
|
// --- Rewrite asset URLs: depth = segments from this HTML file up to output root (website). ----
|
|
733
957
|
// All project assets are copied into output, so root-relative paths become relative within output.
|
|
734
958
|
// Do NOT rewrite href on <a> tags (navigation links); only rewrite link/script/img so router gets clean paths.
|
|
@@ -1222,8 +1446,14 @@ async function runPrerender(config) {
|
|
|
1222
1446
|
const concurrency = config.concurrency;
|
|
1223
1447
|
const pathTotal = pathList.length;
|
|
1224
1448
|
const failedPaths = [];
|
|
1449
|
+
const debugRows = [];
|
|
1225
1450
|
process.stdout.write(`Prerendering ${pathTotal} path(s)...\n`);
|
|
1226
1451
|
|
|
1452
|
+
function pushDebug(row) {
|
|
1453
|
+
if (!config.debugPrerender) return;
|
|
1454
|
+
debugRows.push(row);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1227
1457
|
async function processPath(pathSeg, pathIndex) {
|
|
1228
1458
|
const is404 = pathSeg === NOT_FOUND_PATH;
|
|
1229
1459
|
const pathname = is404 ? `/${NOT_FOUND_PATH}` : (pathSeg ? `/${pathSeg}` : '/');
|
|
@@ -1242,6 +1472,26 @@ async function runPrerender(config) {
|
|
|
1242
1472
|
|
|
1243
1473
|
const page = await browser.newPage();
|
|
1244
1474
|
try {
|
|
1475
|
+
// Align <html lang> with the URL being prerendered before any app script runs.
|
|
1476
|
+
// initializeDataSourcesPlugin picks locale from document.documentElement.lang first; a mismatch
|
|
1477
|
+
// (e.g. headless default vs /en/...) leaves $x.* empty while x-route sections still render.
|
|
1478
|
+
await page.evaluateOnNewDocument((locale) => {
|
|
1479
|
+
const apply = () => {
|
|
1480
|
+
try {
|
|
1481
|
+
if (locale && typeof locale === 'string') document.documentElement.lang = locale;
|
|
1482
|
+
} catch {
|
|
1483
|
+
/* no-op */
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
if (typeof document !== 'undefined') {
|
|
1487
|
+
if (document.readyState === 'loading') {
|
|
1488
|
+
document.addEventListener('DOMContentLoaded', apply, { once: true });
|
|
1489
|
+
}
|
|
1490
|
+
apply();
|
|
1491
|
+
}
|
|
1492
|
+
}, currentLocale);
|
|
1493
|
+
|
|
1494
|
+
pushDebug({ path: displayPath, stage: 'start' });
|
|
1245
1495
|
await page.goto(url, {
|
|
1246
1496
|
waitUntil: 'domcontentloaded',
|
|
1247
1497
|
timeout: Math.min(timeout, 30000),
|
|
@@ -1297,45 +1547,134 @@ async function runPrerender(config) {
|
|
|
1297
1547
|
});
|
|
1298
1548
|
}).catch(() => { });
|
|
1299
1549
|
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
await page
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1550
|
+
// Locale + route sync, then wait for manifest:components-processed (swapping) and settled data stores
|
|
1551
|
+
// before snapshot. Listener is installed inside the page before dispatch (see waitForManifestPrerenderPipeline).
|
|
1552
|
+
await waitForManifestPrerenderPipeline(page, {
|
|
1553
|
+
allLocales: locales,
|
|
1554
|
+
currentLocale,
|
|
1555
|
+
timeoutMs: config.pipelineTimeout,
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
await page.waitForNetworkIdle({ idleTime: 800, timeout: 12000 }).catch(() => { });
|
|
1559
|
+
await waitForAlpineDataStoresSettled(page, 10000);
|
|
1560
|
+
await flushAlpineEffects(page);
|
|
1561
|
+
|
|
1562
|
+
if (config.debugPrerender) {
|
|
1563
|
+
const before = await page.evaluate(() => {
|
|
1564
|
+
const templates = Array.from(document.querySelectorAll('template[x-for]'));
|
|
1565
|
+
const entries = templates.slice(0, 60).map((tpl) => {
|
|
1566
|
+
const first = tpl.content?.firstElementChild;
|
|
1567
|
+
const tag = first ? first.tagName : null;
|
|
1568
|
+
const cls = first ? (first.getAttribute('class') || '') : '';
|
|
1569
|
+
let cloneCount = 0;
|
|
1570
|
+
let next = tpl.nextElementSibling;
|
|
1571
|
+
while (next && (!tag || next.tagName === tag)) {
|
|
1572
|
+
if (tag && (next.getAttribute('class') || '') !== cls) break;
|
|
1573
|
+
cloneCount++;
|
|
1574
|
+
next = next.nextElementSibling;
|
|
1312
1575
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1576
|
+
return {
|
|
1577
|
+
xFor: (tpl.getAttribute('x-for') || '').slice(0, 140),
|
|
1578
|
+
collapsed: tpl.getAttribute('data-prerender-collapsed') === '1',
|
|
1579
|
+
staticGenerated: tpl.getAttribute('data-prerender-static-generated') === '1',
|
|
1580
|
+
cloneCount,
|
|
1581
|
+
};
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
const listDiagnostics = {
|
|
1585
|
+
htmlLang: '',
|
|
1586
|
+
localeCurrent: null,
|
|
1587
|
+
dataLocaleChanging: null,
|
|
1588
|
+
dataStates: {},
|
|
1589
|
+
topLevelArrayLengths: {},
|
|
1590
|
+
nestedContentCards: null,
|
|
1591
|
+
emptyStaticXFors: [],
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
try {
|
|
1595
|
+
listDiagnostics.htmlLang = document.documentElement.lang || '';
|
|
1596
|
+
const Alpine = window.Alpine;
|
|
1597
|
+
if (Alpine?.store) {
|
|
1598
|
+
const loc = Alpine.store('locale');
|
|
1599
|
+
listDiagnostics.localeCurrent = loc?.current ?? null;
|
|
1600
|
+
const d = Alpine.store('data');
|
|
1601
|
+
if (d) {
|
|
1602
|
+
listDiagnostics.dataLocaleChanging = !!d._localeChanging;
|
|
1603
|
+
for (const k of Object.keys(d)) {
|
|
1604
|
+
if (k.startsWith('_') && k.endsWith('_state')) {
|
|
1605
|
+
const short = k.slice(1, -'_state'.length);
|
|
1606
|
+
const s = d[k];
|
|
1607
|
+
if (s && typeof s === 'object') {
|
|
1608
|
+
listDiagnostics.dataStates[short] = {
|
|
1609
|
+
loading: !!s.loading,
|
|
1610
|
+
ready: !!s.ready,
|
|
1611
|
+
hasError: s.error != null,
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
} else if (!k.startsWith('_') && Array.isArray(d[k])) {
|
|
1615
|
+
listDiagnostics.topLevelArrayLengths[k] = d[k].length;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
const cards = d.content?.home?.differentiators?.cards;
|
|
1620
|
+
if (Array.isArray(cards)) listDiagnostics.nestedContentCards = cards.length;
|
|
1621
|
+
else if (cards && typeof cards === 'object') listDiagnostics.nestedContentCards = Object.keys(cards).length;
|
|
1622
|
+
else listDiagnostics.nestedContentCards = cards == null ? null : 'non-iterable';
|
|
1623
|
+
} catch {
|
|
1624
|
+
listDiagnostics.nestedContentCards = 'error';
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1315
1627
|
}
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
listDiagnostics.probeError = String(e?.message || e);
|
|
1316
1630
|
}
|
|
1317
1631
|
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
? '
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
to,
|
|
1330
|
-
normalizedPath: (typeof to === 'string' && to !== '/') ? to.replace(/^\/|\/$/g, '') : '/'
|
|
1632
|
+
for (const tpl of templates) {
|
|
1633
|
+
if (tpl.getAttribute('data-prerender-collapsed') === '1') continue;
|
|
1634
|
+
const first = tpl.content?.firstElementChild;
|
|
1635
|
+
const tag = first ? first.tagName : null;
|
|
1636
|
+
const cls = first ? (first.getAttribute('class') || '') : '';
|
|
1637
|
+
let cloneCount = 0;
|
|
1638
|
+
let next = tpl.nextElementSibling;
|
|
1639
|
+
while (next && (!tag || next.tagName === tag)) {
|
|
1640
|
+
if (tag && (next.getAttribute('class') || '') !== cls) break;
|
|
1641
|
+
cloneCount++;
|
|
1642
|
+
next = next.nextElementSibling;
|
|
1331
1643
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1644
|
+
if (cloneCount > 0) continue;
|
|
1645
|
+
const routeAnc = tpl.closest('[x-route]');
|
|
1646
|
+
let hiddenReason = null;
|
|
1647
|
+
let el = tpl.parentElement;
|
|
1648
|
+
while (el) {
|
|
1649
|
+
if (el.hasAttribute('hidden')) {
|
|
1650
|
+
hiddenReason = 'ancestor-hidden';
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
const st = el.getAttribute('style') || '';
|
|
1654
|
+
if (/\bdisplay\s*:\s*none\b/i.test(st)) {
|
|
1655
|
+
hiddenReason = 'ancestor-display-none';
|
|
1656
|
+
break;
|
|
1657
|
+
}
|
|
1658
|
+
el = el.parentElement;
|
|
1659
|
+
}
|
|
1660
|
+
const itemsHost = tpl.closest('[items]');
|
|
1661
|
+
listDiagnostics.emptyStaticXFors.push({
|
|
1662
|
+
xFor: (tpl.getAttribute('x-for') || '').slice(0, 160),
|
|
1663
|
+
nearestXRoute: routeAnc ? (routeAnc.getAttribute('x-route') || '').slice(0, 100) : null,
|
|
1664
|
+
hiddenReason,
|
|
1665
|
+
hostItemsAttr: itemsHost ? (itemsHost.getAttribute('items') || '').slice(0, 120) : null,
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
return {
|
|
1670
|
+
templateCount: templates.length,
|
|
1671
|
+
nonCollapsedTemplateCount: templates.filter((t) => t.getAttribute('data-prerender-collapsed') !== '1').length,
|
|
1672
|
+
entries,
|
|
1673
|
+
listDiagnostics,
|
|
1674
|
+
};
|
|
1675
|
+
}).catch(() => null);
|
|
1676
|
+
pushDebug({ path: displayPath, stage: 'post-dom-settle', metrics: before });
|
|
1677
|
+
}
|
|
1339
1678
|
|
|
1340
1679
|
// Optional extra delay so in-page async (e.g. fetch() in x-init for client logos) can complete before snapshot.
|
|
1341
1680
|
if (config.waitAfterIdle > 0) {
|
|
@@ -1568,6 +1907,15 @@ async function runPrerender(config) {
|
|
|
1568
1907
|
});
|
|
1569
1908
|
|
|
1570
1909
|
let html = await page.evaluate(() => document.documentElement.outerHTML);
|
|
1910
|
+
if (config.debugPrerender) {
|
|
1911
|
+
const post = await page.evaluate(() => {
|
|
1912
|
+
const templates = document.querySelectorAll('template[x-for]').length;
|
|
1913
|
+
const links = document.querySelectorAll('a[href="#"]').length;
|
|
1914
|
+
const hidden = document.querySelectorAll('[style*="display: none"]').length;
|
|
1915
|
+
return { templateCountAfterCleanup: templates, hashHrefCount: links, displayNoneCount: hidden };
|
|
1916
|
+
}).catch(() => null);
|
|
1917
|
+
pushDebug({ path: displayPath, stage: 'pre-serialize', metrics: post });
|
|
1918
|
+
}
|
|
1571
1919
|
html = stripDevOnlyContent(html);
|
|
1572
1920
|
html = stripInjectedPluginScripts(html);
|
|
1573
1921
|
if (tailwindBuilt) {
|
|
@@ -1587,6 +1935,7 @@ async function runPrerender(config) {
|
|
|
1587
1935
|
const xData = { manifest, content };
|
|
1588
1936
|
html = resolveHeadXBindings(html, xData);
|
|
1589
1937
|
html = stripPrerenderDynamicBindings(html);
|
|
1938
|
+
html = stripEmptyInlineMaskStyles(html);
|
|
1590
1939
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
1591
1940
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
1592
1941
|
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
|
|
@@ -1599,6 +1948,13 @@ async function runPrerender(config) {
|
|
|
1599
1948
|
html = html.replace('</head>', `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`);
|
|
1600
1949
|
mkdirSync(outDir, { recursive: true });
|
|
1601
1950
|
writeFileSync(outFile, html, 'utf8');
|
|
1951
|
+
pushDebug({
|
|
1952
|
+
path: displayPath,
|
|
1953
|
+
stage: 'wrote',
|
|
1954
|
+
outFile,
|
|
1955
|
+
htmlBytes: Buffer.byteLength(html, 'utf8'),
|
|
1956
|
+
hasXForTemplate: html.includes('template x-for') || html.includes('template[x-for]'),
|
|
1957
|
+
});
|
|
1602
1958
|
} catch (err) {
|
|
1603
1959
|
failedPaths.push({
|
|
1604
1960
|
path: displayPath,
|
|
@@ -1633,6 +1989,17 @@ async function runPrerender(config) {
|
|
|
1633
1989
|
throw new Error(`prerender failed for ${failedPaths.length}/${pathTotal} paths. Sample: ${sample}`);
|
|
1634
1990
|
}
|
|
1635
1991
|
|
|
1992
|
+
if (config.debugPrerender) {
|
|
1993
|
+
const reportPath = join(outputResolved, 'prerender.debug.json');
|
|
1994
|
+
writeFileSync(reportPath, JSON.stringify({
|
|
1995
|
+
generatedAt: new Date().toISOString(),
|
|
1996
|
+
totalPaths: pathTotal,
|
|
1997
|
+
failedPaths,
|
|
1998
|
+
rows: debugRows,
|
|
1999
|
+
}, null, 2), 'utf8');
|
|
2000
|
+
process.stdout.write(`prerender: debug report ${reportPath}\n`);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1636
2003
|
if (bundleUtilities) {
|
|
1637
2004
|
const utilMerged = mergeUtilityCssBlocks(utilityBlocks);
|
|
1638
2005
|
if (utilMerged.trim()) {
|