mnfst-render 0.2.7 → 0.2.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 +309 -56
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -23,6 +23,180 @@ 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: run component swapping explicitly, then wait until Alpine data stores
|
|
81
|
+
* are settled. We call ManifestComponentsSwapping.processAll() directly because swapping only
|
|
82
|
+
* subscribes to manifest:route-change if window.ManifestRouting already exists at initialize() time;
|
|
83
|
+
* when the router loads after components, that listener is never registered and no second
|
|
84
|
+
* manifest:components-processed fires.
|
|
85
|
+
*/
|
|
86
|
+
async function waitForManifestPrerenderPipeline(page, { allLocales, currentLocale, timeoutMs }) {
|
|
87
|
+
const result = await page
|
|
88
|
+
.evaluate(
|
|
89
|
+
async ({ localeList, loc, ms }) => {
|
|
90
|
+
const sleep = (n) => new Promise((r) => setTimeout(r, n));
|
|
91
|
+
const deadline = Date.now() + ms;
|
|
92
|
+
|
|
93
|
+
const checkData = () => {
|
|
94
|
+
try {
|
|
95
|
+
const Alpine = window.Alpine;
|
|
96
|
+
if (!Alpine?.store) return true;
|
|
97
|
+
const d = Alpine.store('data');
|
|
98
|
+
if (!d) return true;
|
|
99
|
+
if (d._localeChanging) return false;
|
|
100
|
+
for (const k of Object.keys(d)) {
|
|
101
|
+
if (!k.startsWith('_') || !k.endsWith('_state')) continue;
|
|
102
|
+
const s = d[k];
|
|
103
|
+
if (s && typeof s === 'object' && s.loading) return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const locales = Array.isArray(localeList) ? localeList : [];
|
|
113
|
+
if (loc && typeof loc === 'string') {
|
|
114
|
+
try {
|
|
115
|
+
document.documentElement.lang = loc;
|
|
116
|
+
} catch {
|
|
117
|
+
/* no-op */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const store = typeof Alpine !== 'undefined' && Alpine.store ? Alpine.store('locale') : null;
|
|
121
|
+
if (store) {
|
|
122
|
+
if (!Array.isArray(store.available) || store.available.length === 0) {
|
|
123
|
+
store.available = locales.slice();
|
|
124
|
+
} else {
|
|
125
|
+
store.available = Array.from(new Set([...store.available, ...locales]));
|
|
126
|
+
}
|
|
127
|
+
if (loc && typeof loc === 'string') {
|
|
128
|
+
store.current = loc;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (loc && typeof loc === 'string') {
|
|
133
|
+
window.dispatchEvent(new CustomEvent('localechange', { detail: { locale: loc } }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rawRoute = window.ManifestRoutingNavigation?.getCurrentRoute?.() ?? window.location.pathname;
|
|
137
|
+
const clean = String(rawRoute || '/').replace(/^\/+|\/+$/g, '');
|
|
138
|
+
const parts = clean ? clean.split('/') : [];
|
|
139
|
+
const logical =
|
|
140
|
+
parts.length > 0 && locales.includes(parts[0])
|
|
141
|
+
? '/' + parts.slice(1).join('/')
|
|
142
|
+
: clean
|
|
143
|
+
? '/' + clean
|
|
144
|
+
: '/';
|
|
145
|
+
const to = logical === '//' ? '/' : logical;
|
|
146
|
+
const normalizedPath =
|
|
147
|
+
typeof to === 'string' && to !== '/' ? to.replace(/^\/|\/$/g, '') : '/';
|
|
148
|
+
|
|
149
|
+
window.dispatchEvent(
|
|
150
|
+
new CustomEvent('manifest:route-change', {
|
|
151
|
+
detail: {
|
|
152
|
+
from: to,
|
|
153
|
+
to,
|
|
154
|
+
normalizedPath,
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
159
|
+
|
|
160
|
+
if (window.ManifestComponentsSwapping?.processAll) {
|
|
161
|
+
try {
|
|
162
|
+
await window.ManifestComponentsSwapping.processAll(normalizedPath);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return { ok: false, reason: 'processAll-error', message: String(e?.message || e) };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
while (Date.now() < deadline) {
|
|
169
|
+
if (checkData()) {
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
await sleep(50);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: 'timeout',
|
|
177
|
+
dataOk: checkData(),
|
|
178
|
+
hadSwapping: !!window.ManifestComponentsSwapping?.processAll,
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return { ok: false, reason: 'error', message: String(err?.message || err) };
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
localeList: allLocales,
|
|
186
|
+
loc: currentLocale,
|
|
187
|
+
ms: timeoutMs,
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
.catch((e) => ({ ok: false, reason: 'evaluate', message: String(e) }));
|
|
191
|
+
|
|
192
|
+
if (!result?.ok) {
|
|
193
|
+
const parts = [`prerender: pipeline wait incomplete (${result?.reason ?? 'unknown'})`];
|
|
194
|
+
if (result?.dataOk === false) parts.push('data still loading');
|
|
195
|
+
if (result?.message) parts.push(result.message);
|
|
196
|
+
process.stdout.write(`${parts.join('; ')}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
26
200
|
// --- Config ------------------------------------------------------------------
|
|
27
201
|
|
|
28
202
|
function parseArgs() {
|
|
@@ -88,6 +262,7 @@ function resolveConfig() {
|
|
|
88
262
|
concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? 6),
|
|
89
263
|
dryRun: !!cli.dryRun,
|
|
90
264
|
debugPrerender: !!(cli.debugPrerender ?? pre.debugPrerender),
|
|
265
|
+
pipelineTimeout: Math.max(3000, Number(pre.pipelineTimeout) || 25000),
|
|
91
266
|
};
|
|
92
267
|
}
|
|
93
268
|
|
|
@@ -1273,6 +1448,25 @@ async function runPrerender(config) {
|
|
|
1273
1448
|
|
|
1274
1449
|
const page = await browser.newPage();
|
|
1275
1450
|
try {
|
|
1451
|
+
// Align <html lang> with the URL being prerendered before any app script runs.
|
|
1452
|
+
// initializeDataSourcesPlugin picks locale from document.documentElement.lang first; a mismatch
|
|
1453
|
+
// (e.g. headless default vs /en/...) leaves $x.* empty while x-route sections still render.
|
|
1454
|
+
await page.evaluateOnNewDocument((locale) => {
|
|
1455
|
+
const apply = () => {
|
|
1456
|
+
try {
|
|
1457
|
+
if (locale && typeof locale === 'string') document.documentElement.lang = locale;
|
|
1458
|
+
} catch {
|
|
1459
|
+
/* no-op */
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
if (typeof document !== 'undefined') {
|
|
1463
|
+
if (document.readyState === 'loading') {
|
|
1464
|
+
document.addEventListener('DOMContentLoaded', apply, { once: true });
|
|
1465
|
+
}
|
|
1466
|
+
apply();
|
|
1467
|
+
}
|
|
1468
|
+
}, currentLocale);
|
|
1469
|
+
|
|
1276
1470
|
pushDebug({ path: displayPath, stage: 'start' });
|
|
1277
1471
|
await page.goto(url, {
|
|
1278
1472
|
waitUntil: 'domcontentloaded',
|
|
@@ -1313,6 +1507,34 @@ async function runPrerender(config) {
|
|
|
1313
1507
|
});
|
|
1314
1508
|
}).catch(() => { });
|
|
1315
1509
|
|
|
1510
|
+
await page.waitForNetworkIdle({ idleTime: 1500, timeout: 10000 }).catch(() => { });
|
|
1511
|
+
|
|
1512
|
+
await page.evaluate(() => {
|
|
1513
|
+
return new Promise((resolve) => {
|
|
1514
|
+
const observer = new MutationObserver(() => {
|
|
1515
|
+
clearTimeout(stable);
|
|
1516
|
+
stable = setTimeout(resolve, 800);
|
|
1517
|
+
});
|
|
1518
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
1519
|
+
let stable = setTimeout(() => {
|
|
1520
|
+
observer.disconnect();
|
|
1521
|
+
resolve();
|
|
1522
|
+
}, 800);
|
|
1523
|
+
});
|
|
1524
|
+
}).catch(() => { });
|
|
1525
|
+
|
|
1526
|
+
// Locale + route sync, then wait for manifest:components-processed (swapping) and settled data stores
|
|
1527
|
+
// before snapshot. Listener is installed inside the page before dispatch (see waitForManifestPrerenderPipeline).
|
|
1528
|
+
await waitForManifestPrerenderPipeline(page, {
|
|
1529
|
+
allLocales: locales,
|
|
1530
|
+
currentLocale,
|
|
1531
|
+
timeoutMs: config.pipelineTimeout,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
await page.waitForNetworkIdle({ idleTime: 800, timeout: 12000 }).catch(() => { });
|
|
1535
|
+
await waitForAlpineDataStoresSettled(page, 10000);
|
|
1536
|
+
await flushAlpineEffects(page);
|
|
1537
|
+
|
|
1316
1538
|
if (config.debugPrerender) {
|
|
1317
1539
|
const before = await page.evaluate(() => {
|
|
1318
1540
|
const templates = Array.from(document.querySelectorAll('template[x-for]'));
|
|
@@ -1334,71 +1556,102 @@ async function runPrerender(config) {
|
|
|
1334
1556
|
cloneCount,
|
|
1335
1557
|
};
|
|
1336
1558
|
});
|
|
1559
|
+
|
|
1560
|
+
const listDiagnostics = {
|
|
1561
|
+
htmlLang: '',
|
|
1562
|
+
localeCurrent: null,
|
|
1563
|
+
dataLocaleChanging: null,
|
|
1564
|
+
dataStates: {},
|
|
1565
|
+
topLevelArrayLengths: {},
|
|
1566
|
+
nestedContentCards: null,
|
|
1567
|
+
emptyStaticXFors: [],
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
listDiagnostics.htmlLang = document.documentElement.lang || '';
|
|
1572
|
+
const Alpine = window.Alpine;
|
|
1573
|
+
if (Alpine?.store) {
|
|
1574
|
+
const loc = Alpine.store('locale');
|
|
1575
|
+
listDiagnostics.localeCurrent = loc?.current ?? null;
|
|
1576
|
+
const d = Alpine.store('data');
|
|
1577
|
+
if (d) {
|
|
1578
|
+
listDiagnostics.dataLocaleChanging = !!d._localeChanging;
|
|
1579
|
+
for (const k of Object.keys(d)) {
|
|
1580
|
+
if (k.startsWith('_') && k.endsWith('_state')) {
|
|
1581
|
+
const short = k.slice(1, -'_state'.length);
|
|
1582
|
+
const s = d[k];
|
|
1583
|
+
if (s && typeof s === 'object') {
|
|
1584
|
+
listDiagnostics.dataStates[short] = {
|
|
1585
|
+
loading: !!s.loading,
|
|
1586
|
+
ready: !!s.ready,
|
|
1587
|
+
hasError: s.error != null,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
} else if (!k.startsWith('_') && Array.isArray(d[k])) {
|
|
1591
|
+
listDiagnostics.topLevelArrayLengths[k] = d[k].length;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
try {
|
|
1595
|
+
const cards = d.content?.home?.differentiators?.cards;
|
|
1596
|
+
if (Array.isArray(cards)) listDiagnostics.nestedContentCards = cards.length;
|
|
1597
|
+
else if (cards && typeof cards === 'object') listDiagnostics.nestedContentCards = Object.keys(cards).length;
|
|
1598
|
+
else listDiagnostics.nestedContentCards = cards == null ? null : 'non-iterable';
|
|
1599
|
+
} catch {
|
|
1600
|
+
listDiagnostics.nestedContentCards = 'error';
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
} catch (e) {
|
|
1605
|
+
listDiagnostics.probeError = String(e?.message || e);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
for (const tpl of templates) {
|
|
1609
|
+
if (tpl.getAttribute('data-prerender-collapsed') === '1') continue;
|
|
1610
|
+
const first = tpl.content?.firstElementChild;
|
|
1611
|
+
const tag = first ? first.tagName : null;
|
|
1612
|
+
const cls = first ? (first.getAttribute('class') || '') : '';
|
|
1613
|
+
let cloneCount = 0;
|
|
1614
|
+
let next = tpl.nextElementSibling;
|
|
1615
|
+
while (next && (!tag || next.tagName === tag)) {
|
|
1616
|
+
if (tag && (next.getAttribute('class') || '') !== cls) break;
|
|
1617
|
+
cloneCount++;
|
|
1618
|
+
next = next.nextElementSibling;
|
|
1619
|
+
}
|
|
1620
|
+
if (cloneCount > 0) continue;
|
|
1621
|
+
const routeAnc = tpl.closest('[x-route]');
|
|
1622
|
+
let hiddenReason = null;
|
|
1623
|
+
let el = tpl.parentElement;
|
|
1624
|
+
while (el) {
|
|
1625
|
+
if (el.hasAttribute('hidden')) {
|
|
1626
|
+
hiddenReason = 'ancestor-hidden';
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
const st = el.getAttribute('style') || '';
|
|
1630
|
+
if (/\bdisplay\s*:\s*none\b/i.test(st)) {
|
|
1631
|
+
hiddenReason = 'ancestor-display-none';
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
el = el.parentElement;
|
|
1635
|
+
}
|
|
1636
|
+
const itemsHost = tpl.closest('[items]');
|
|
1637
|
+
listDiagnostics.emptyStaticXFors.push({
|
|
1638
|
+
xFor: (tpl.getAttribute('x-for') || '').slice(0, 160),
|
|
1639
|
+
nearestXRoute: routeAnc ? (routeAnc.getAttribute('x-route') || '').slice(0, 100) : null,
|
|
1640
|
+
hiddenReason,
|
|
1641
|
+
hostItemsAttr: itemsHost ? (itemsHost.getAttribute('items') || '').slice(0, 120) : null,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1337
1645
|
return {
|
|
1338
1646
|
templateCount: templates.length,
|
|
1339
1647
|
nonCollapsedTemplateCount: templates.filter((t) => t.getAttribute('data-prerender-collapsed') !== '1').length,
|
|
1340
1648
|
entries,
|
|
1649
|
+
listDiagnostics,
|
|
1341
1650
|
};
|
|
1342
1651
|
}).catch(() => null);
|
|
1343
1652
|
pushDebug({ path: displayPath, stage: 'post-dom-settle', metrics: before });
|
|
1344
1653
|
}
|
|
1345
1654
|
|
|
1346
|
-
await page.waitForNetworkIdle({ idleTime: 1500, timeout: 10000 }).catch(() => { });
|
|
1347
|
-
|
|
1348
|
-
await page.evaluate(() => {
|
|
1349
|
-
return new Promise((resolve) => {
|
|
1350
|
-
const observer = new MutationObserver(() => {
|
|
1351
|
-
clearTimeout(stable);
|
|
1352
|
-
stable = setTimeout(resolve, 800);
|
|
1353
|
-
});
|
|
1354
|
-
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
1355
|
-
let stable = setTimeout(() => {
|
|
1356
|
-
observer.disconnect();
|
|
1357
|
-
resolve();
|
|
1358
|
-
}, 800);
|
|
1359
|
-
});
|
|
1360
|
-
}).catch(() => { });
|
|
1361
|
-
|
|
1362
|
-
// Ensure $route-dependent expressions are recalculated after locale/data stores settle.
|
|
1363
|
-
// This helps localized dynamic pages (e.g. /ko/articles/slug) compute prev/next links correctly.
|
|
1364
|
-
await page.evaluate(({ allLocales, currentLocale }) => {
|
|
1365
|
-
try {
|
|
1366
|
-
const localeList = Array.isArray(allLocales) ? allLocales : [];
|
|
1367
|
-
const store = (typeof Alpine !== 'undefined' && Alpine.store) ? Alpine.store('locale') : null;
|
|
1368
|
-
if (store) {
|
|
1369
|
-
if (!Array.isArray(store.available) || store.available.length === 0) {
|
|
1370
|
-
store.available = localeList.slice();
|
|
1371
|
-
} else {
|
|
1372
|
-
const merged = Array.from(new Set([...store.available, ...localeList]));
|
|
1373
|
-
store.available = merged;
|
|
1374
|
-
}
|
|
1375
|
-
if (currentLocale && typeof currentLocale === 'string') {
|
|
1376
|
-
store.current = currentLocale;
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
const rawRoute = window.ManifestRoutingNavigation?.getCurrentRoute?.() ?? window.location.pathname;
|
|
1381
|
-
const clean = String(rawRoute || '/').replace(/^\/+|\/+$/g, '');
|
|
1382
|
-
const parts = clean ? clean.split('/') : [];
|
|
1383
|
-
const logical = parts.length > 0 && localeList.includes(parts[0])
|
|
1384
|
-
? '/' + parts.slice(1).join('/')
|
|
1385
|
-
: (clean ? '/' + clean : '/');
|
|
1386
|
-
const to = logical === '//' ? '/' : logical;
|
|
1387
|
-
|
|
1388
|
-
window.dispatchEvent(new CustomEvent('manifest:route-change', {
|
|
1389
|
-
detail: {
|
|
1390
|
-
from: to,
|
|
1391
|
-
to,
|
|
1392
|
-
normalizedPath: (typeof to === 'string' && to !== '/') ? to.replace(/^\/|\/$/g, '') : '/'
|
|
1393
|
-
}
|
|
1394
|
-
}));
|
|
1395
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
1396
|
-
} catch {
|
|
1397
|
-
// no-op
|
|
1398
|
-
}
|
|
1399
|
-
}, { allLocales: locales, currentLocale }).catch(() => { });
|
|
1400
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
1401
|
-
|
|
1402
1655
|
// Optional extra delay so in-page async (e.g. fetch() in x-init for client logos) can complete before snapshot.
|
|
1403
1656
|
if (config.waitAfterIdle > 0) {
|
|
1404
1657
|
await new Promise((r) => setTimeout(r, config.waitAfterIdle));
|