mnfst-render 0.5.5 → 0.5.7

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.
Files changed (2) hide show
  1. package/manifest.render.mjs +61 -27
  2. package/package.json +1 -1
@@ -150,11 +150,11 @@ async function waitForManifestRenderReady(page, { allLocales, currentLocale, tim
150
150
  )
151
151
  .catch((e) => ({ ok: false, reason: 'evaluate', message: String(e) }));
152
152
 
153
- if (!result?.ok) {
154
- const parts = [`prerender: render-ready wait incomplete (${result?.reason ?? 'unknown'})`];
155
- if (result?.message) parts.push(result.message);
156
- process.stdout.write(`${parts.join('; ')}\n`);
157
- }
153
+ // Note: render-ready wait timeouts are silently tolerated. Earlier versions
154
+ // logged a warning per path, but it fires on essentially every route in
155
+ // projects whose data plugins don't dispatch `manifest:render-ready` (i.e.
156
+ // most of them), drowning the terminal in noise. The fallback timeout is
157
+ // intentional and benign — the DOM is still captured.
158
158
  }
159
159
 
160
160
  // --- Config ------------------------------------------------------------------
@@ -246,7 +246,11 @@ function resolveConfig() {
246
246
  : [],
247
247
  dryRun: !!cli.dryRun,
248
248
  debugPrerender: !!cli.debugPrerender,
249
- pipelineTimeout: 25000,
249
+ // Cap on the manifest:render-ready wait. When the data plugin dispatches
250
+ // the event, we resolve immediately; when it doesn't (most projects), we
251
+ // fall back to the timeout. Lowered from 25000 — the earlier settle
252
+ // waits already give plugins enough time to finish.
253
+ pipelineTimeout: 6000,
250
254
  };
251
255
  }
252
256
 
@@ -1356,7 +1360,7 @@ function generateLocaleVariantHtml({
1356
1360
  html = stripEmptyInlineMaskStyles(html);
1357
1361
  html = stripResolvedXIconDirectives(html);
1358
1362
  // markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
1359
- // detect data-prerender-hydrate markers and skip components inside hydrate islands.
1363
+ // detect data-hydrate markers and skip components inside hydrate islands.
1360
1364
  html = markPrerenderedManifestComponents(html);
1361
1365
 
1362
1366
  const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
@@ -2024,16 +2028,21 @@ async function runPrerender(config) {
2024
2028
  timeout: Math.min(timeout, 30000),
2025
2029
  });
2026
2030
 
2031
+ // Belt-and-suspenders settle waits. Each one is bounded short because
2032
+ // the authoritative `manifest:render-ready` wait below is what we really
2033
+ // rely on. These earlier waits exist as fallbacks for projects whose
2034
+ // data plugins don't dispatch the modern signal — but they shouldn't
2035
+ // dominate per-path runtime when they DO time out.
2027
2036
  await Promise.race([
2028
2037
  page.evaluate(() => {
2029
2038
  return new Promise((resolve) => {
2030
2039
  const done = () => resolve();
2031
- const t = setTimeout(done, 6000);
2040
+ const t = setTimeout(done, 1500); // was 6000
2032
2041
  window.addEventListener(
2033
2042
  'manifest:routing-ready',
2034
2043
  () => {
2035
2044
  clearTimeout(t);
2036
- setTimeout(done, 2000);
2045
+ setTimeout(done, 500); // was 2000
2037
2046
  },
2038
2047
  { once: true }
2039
2048
  );
@@ -2042,13 +2051,13 @@ async function runPrerender(config) {
2042
2051
  new Promise((_, rej) => setTimeout(() => rej(new Error('ready timeout')), timeout)),
2043
2052
  ]).catch(() => { });
2044
2053
 
2045
- // Ensure manifest.min.js (dynamic loader) has run and injected plugin scripts before snapshot.
2046
- // Static output still runs the loader and Alpine; we just capture the DOM after they've set up.
2054
+ // Ensure the dynamic loader has injected at least one plugin script.
2055
+ // In practice this happens within ~100ms; the 1500ms cap is generous.
2047
2056
  await page.evaluate(() => {
2048
2057
  return new Promise((resolve) => {
2049
2058
  const check = () => document.querySelectorAll('script[src*="manifest"]').length >= 2;
2050
2059
  if (check()) return resolve();
2051
- const deadline = Date.now() + 5000;
2060
+ const deadline = Date.now() + 1500; // was 5000
2052
2061
  const t = setInterval(() => {
2053
2062
  if (check() || Date.now() >= deadline) {
2054
2063
  clearInterval(t);
@@ -2058,19 +2067,21 @@ async function runPrerender(config) {
2058
2067
  });
2059
2068
  }).catch(() => { });
2060
2069
 
2061
- await page.waitForNetworkIdle({ idleTime: 1500, timeout: 10000 }).catch(() => { });
2070
+ // Network idle: shorter idle window + shorter cap. Most plugin scripts
2071
+ // and data fetches complete in under 2s on a healthy local server.
2072
+ await page.waitForNetworkIdle({ idleTime: 800, timeout: 4000 }).catch(() => { });
2062
2073
 
2074
+ // DOM stability: 300ms quiet window is plenty after networkIdle has
2075
+ // already drained pending fetches.
2063
2076
  await page.evaluate(() => {
2064
2077
  return new Promise((resolve) => {
2065
2078
  const observer = new MutationObserver(() => {
2066
2079
  clearTimeout(stable);
2067
- stable = setTimeout(resolve, 800);
2080
+ stable = setTimeout(finish, 300); // was 800
2068
2081
  });
2069
2082
  observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
2070
- let stable = setTimeout(() => {
2071
- observer.disconnect();
2072
- resolve();
2073
- }, 800);
2083
+ const finish = () => { observer.disconnect(); resolve(); };
2084
+ let stable = setTimeout(finish, 300); // was 800
2074
2085
  });
2075
2086
  }).catch(() => { });
2076
2087
 
@@ -2239,13 +2250,36 @@ async function runPrerender(config) {
2239
2250
  });
2240
2251
 
2241
2252
  // Strip x-markdown from elements that already have baked content.
2242
- // The markdown plugin hides elements with opacity:0 on init, then re-fetches and re-renders.
2243
- // For prerendered pages the content is already baked — removing x-markdown prevents the
2244
- // runtime plugin from re-processing (and temporarily hiding) the static content.
2253
+ // The markdown plugin hides elements with opacity:0 on init, then re-fetches
2254
+ // and re-renders. For prerendered pages the content is already baked —
2255
+ // removing x-markdown prevents the runtime plugin from re-processing (and
2256
+ // temporarily hiding) the static content.
2257
+ //
2258
+ // We ALSO clear any leftover `opacity: 0` inline style the plugin set
2259
+ // before/while rendering. On dynamic expressions that initially evaluate
2260
+ // empty (e.g. article content keyed off `$route` before `$x.articles` has
2261
+ // loaded), the plugin sets opacity to 0 and may never restore it to 1 if
2262
+ // the effect re-fires with an empty value. The end state in the
2263
+ // serialized HTML has rendered content but opacity:0 — invisible in
2264
+ // production. Since we're also removing x-markdown (so the runtime
2265
+ // plugin doesn't re-hide the element), leaving the inline style would
2266
+ // permanently hide authored content.
2245
2267
  await page.evaluate(() => {
2246
2268
  document.querySelectorAll('[x-markdown]').forEach((el) => {
2247
2269
  if (!el.textContent.trim() && !el.innerHTML.trim()) return;
2248
2270
  el.removeAttribute('x-markdown');
2271
+ // Clean up opacity-0 + transition inline styles the plugin left behind.
2272
+ const style = el.getAttribute('style') || '';
2273
+ if (style) {
2274
+ const cleaned = style
2275
+ .replace(/\bopacity\s*:\s*0(?:\.\d+)?\s*;?/gi, '')
2276
+ .replace(/\btransition\s*:\s*opacity[^;]*;?/gi, '')
2277
+ .replace(/;\s*;/g, ';')
2278
+ .replace(/^\s*;\s*|\s*;\s*$/g, '')
2279
+ .trim();
2280
+ if (cleaned) el.setAttribute('style', cleaned);
2281
+ else el.removeAttribute('style');
2282
+ }
2249
2283
  });
2250
2284
  });
2251
2285
 
@@ -2404,7 +2438,7 @@ async function runPrerender(config) {
2404
2438
  // $url, $auth, or iterates over getter names (filtered*, results, searchResults). See docs prerender + local.data.
2405
2439
  await page.evaluate(() => {
2406
2440
  document.querySelectorAll('template[x-for]').forEach((tpl) => {
2407
- if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) {
2441
+ if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) {
2408
2442
  tpl.removeAttribute('data-prerender-collapsed');
2409
2443
  tpl.removeAttribute('data-prerender-static-generated');
2410
2444
  return;
@@ -2485,7 +2519,7 @@ async function runPrerender(config) {
2485
2519
  await page.evaluate(() => {
2486
2520
  const loopVarRx = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
2487
2521
  document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
2488
- if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
2522
+ if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
2489
2523
  const xFor = (tpl.getAttribute('x-for') || '').trim();
2490
2524
  const m = xFor.match(loopVarRx);
2491
2525
  const itemVar = m ? (m[1] || m[3] || '') : '';
@@ -2499,7 +2533,7 @@ async function runPrerender(config) {
2499
2533
  // Only process clones that contain data-hydrate descendants
2500
2534
  if (
2501
2535
  !n.hasAttribute('x-data') &&
2502
- (n.hasAttribute('data-prerender-hydrate') || n.querySelector('[data-prerender-hydrate]'))
2536
+ (n.hasAttribute('data-hydrate') || n.querySelector('[data-hydrate]'))
2503
2537
  ) {
2504
2538
  try {
2505
2539
  const A = window.Alpine;
@@ -2541,7 +2575,7 @@ async function runPrerender(config) {
2541
2575
  const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
2542
2576
  for (const node of nodes) {
2543
2577
  // Skip elements inside data-hydrate islands — their bindings must remain live
2544
- if (node.hasAttribute('data-prerender-hydrate') || node.closest('[data-prerender-hydrate]')) continue;
2578
+ if (node.hasAttribute('data-hydrate') || node.closest('[data-hydrate]')) continue;
2545
2579
  const attrs = node.attributes ? Array.from(node.attributes) : [];
2546
2580
  for (const attr of attrs) {
2547
2581
  if (!bindingAttrRegex.test(attr.name)) continue;
@@ -2579,7 +2613,7 @@ async function runPrerender(config) {
2579
2613
  };
2580
2614
 
2581
2615
  document.querySelectorAll('template[x-for]').forEach((tpl) => {
2582
- if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
2616
+ if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
2583
2617
  const xFor = (tpl.getAttribute('x-for') || '').trim();
2584
2618
  const m = xFor.match(loopVarRegex);
2585
2619
  const itemVar = m ? (m[1] || m[3] || '') : '';
@@ -2608,7 +2642,7 @@ async function runPrerender(config) {
2608
2642
  const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
2609
2643
  runBatch(() => {
2610
2644
  document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
2611
- if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
2645
+ if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
2612
2646
  const parent = tpl.parentNode;
2613
2647
  if (!parent) {
2614
2648
  tpl.remove();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {