mnfst-render 0.5.14 → 0.5.16

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 +48 -27
  2. package/package.json +1 -1
@@ -609,26 +609,46 @@ function stripDevOnlyContent(html) {
609
609
  return out;
610
610
  }
611
611
 
612
- // --- Strip plugin scripts injected by the loader during prerender so only the loader tag remains ---
613
- // When the static page loads, the loader runs once and adds plugins; avoids
614
- // duplicate script execution (which would cause `const` re-declaration errors).
615
- // Matches both CDN-minified (.min.js) and self-hosted (.js) plugin URLs.
616
- // Also strips the loader-injected Alpine script (both defer and non-defer
617
- // forms) at runtime the loader re-injects Alpine AFTER plugin registration,
618
- // and if Chromium serialized an Alpine script tag during the Puppeteer render,
619
- // leaving it in place would cause Alpine to execute synchronously during HTML
620
- // parse, before plugins have a chance to register their directives.
621
- function stripInjectedPluginScripts(html) {
622
- const pluginPattern =
623
- /<script[^>]*\ssrc=["'][^"']*manifest\.(?:components|router|utilities|data|icons|localization|markdown|code|themes|toasts|tooltips|dropdowns|tabs|slides|resize|colorpicker|tailwind|appwrite\.(?:auth|data|presence))[^"']*\.(?:min\.)?js["'][^>]*>\s*<\/script>/gi;
624
- let out = html.replace(pluginPattern, '');
625
- const alpinePattern =
626
- /<script[^>]*\ssrc=["'][^"']*\/alpinejs@[^"']*["'][^>]*>\s*<\/script>/gi;
627
- out = out.replace(alpinePattern, '');
628
- const runtimePattern =
629
- /<script[^>]*\ssrc=["'][^"']*(?:papaparse@[^"']*\/papaparse\.min\.js|marked\/marked\.min\.js|highlightjs\/cdn-release@[^"']*\/highlight\.min\.js)[^"']*["'][^>]*>\s*<\/script>/gi;
630
- out = out.replace(runtimePattern, '');
631
- return out;
612
+ // --- Strip scripts injected at runtime during prerender ---
613
+ // The Manifest loader, Alpine, plugins, and third-party libraries inject
614
+ // <script> tags into the DOM during the Puppeteer render. These must be
615
+ // removed from the serialized HTML so the loader can re-inject them fresh
616
+ // at runtime (otherwise the addScript function finds an existing tag, waits
617
+ // for a load event that already fired, and hangs forever).
618
+ //
619
+ // Approach: diff the prerendered HTML against the ORIGINAL index.html from
620
+ // disk. Any <script src="..."> whose src does NOT appear in the original
621
+ // file was injected at runtime and must be stripped. Inline scripts without
622
+ // src are left alone (author-written analytics snippets, etc.).
623
+ //
624
+ // This is future-proof — new framework plugins, Alpine version bumps, and
625
+ // arbitrary third-party scripts (webchat, analytics) are all handled
626
+ // automatically without maintaining a hardcoded allowlist.
627
+ let _originalScriptSrcs = null;
628
+
629
+ function buildOriginalScriptSrcSet(rootDir) {
630
+ if (_originalScriptSrcs) return _originalScriptSrcs;
631
+ _originalScriptSrcs = new Set();
632
+ const indexPath = join(rootDir, 'index.html');
633
+ if (!existsSync(indexPath)) return _originalScriptSrcs;
634
+ const html = readFileSync(indexPath, 'utf8');
635
+ const srcPattern = /<script[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
636
+ let m;
637
+ while ((m = srcPattern.exec(html)) !== null) {
638
+ _originalScriptSrcs.add(m[1]);
639
+ }
640
+ return _originalScriptSrcs;
641
+ }
642
+
643
+ function stripInjectedPluginScripts(html, rootDir) {
644
+ const originals = buildOriginalScriptSrcSet(rootDir);
645
+ // Remove every <script src="...">...</script> whose src is NOT in the
646
+ // original index.html. This catches all loader-injected plugins, Alpine,
647
+ // runtime libraries (js-yaml, marked, highlight, etc.), and any third-party
648
+ // scripts added dynamically during the render.
649
+ return html.replace(/<script[^>]*\ssrc=["']([^"']+)["'][^>]*>\s*<\/script>/gi,
650
+ (full, src) => originals.has(src) ? full : ''
651
+ );
632
652
  }
633
653
 
634
654
  function stripRuntimeTailwindArtifacts(html) {
@@ -1385,7 +1405,7 @@ function generateLocaleVariantHtml({
1385
1405
 
1386
1406
  // Standard Node.js post-processing (same sequence as processPath)
1387
1407
  html = stripDevOnlyContent(html);
1388
- html = stripInjectedPluginScripts(html);
1408
+ html = stripInjectedPluginScripts(html, config.root);
1389
1409
  if (tailwindBuilt) html = stripRuntimeTailwindArtifacts(html);
1390
1410
 
1391
1411
  const pageUtilityBlocks = [];
@@ -2722,13 +2742,14 @@ async function runPrerender(config) {
2722
2742
  runBatch(() => {
2723
2743
  document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
2724
2744
  if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
2725
- // Keep templates whose x-for iterates over $x data these are data-driven
2726
- // lists that need Alpine to re-render at runtime (e.g. for locale changes,
2727
- // filtering). Remove the STATIC CLONES so Alpine's re-render from the
2728
- // template doesn't produce duplicates, but the template itself stays.
2745
+ // $x-driven x-for: keep the template so Alpine can re-render the
2746
+ // list at runtime (locale switching, filtering, etc.), but remove
2747
+ // the static clones Alpine creates fresh clones on init and does
2748
+ // NOT adopt existing DOM nodes, so leaving them produces duplicates.
2749
+ // Individual article/pricing pages still have full baked content
2750
+ // (via x-text/x-html); the x-for list is only the index/grid view.
2729
2751
  const xFor = (tpl.getAttribute('x-for') || '');
2730
2752
  if (xFor.includes('$x')) {
2731
- // Remove static clones (siblings with same tag+class as template's first child)
2732
2753
  const first = tpl.content?.firstElementChild;
2733
2754
  if (first) {
2734
2755
  const tag = first.tagName;
@@ -2860,7 +2881,7 @@ async function runPrerender(config) {
2860
2881
  pushDebug({ path: displayPath, stage: 'pre-serialize', metrics: post });
2861
2882
  }
2862
2883
  html = stripDevOnlyContent(html);
2863
- html = stripInjectedPluginScripts(html);
2884
+ html = stripInjectedPluginScripts(html, config.root);
2864
2885
  if (tailwindBuilt) {
2865
2886
  html = stripRuntimeTailwindArtifacts(html);
2866
2887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {