mnfst-render 0.5.15 → 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 +42 -58
  2. package/package.json +1 -1
@@ -609,32 +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
- // Safety-net regex strip for any Manifest plugin scripts that survived the
623
- // DOM-level removal in the Puppeteer evaluate phase. Uses a broad pattern
624
- // that matches ANY `manifest.*.js` or `manifest.*.min.js` script no need
625
- // to enumerate individual plugin names. New plugins are covered automatically.
626
- const pluginPattern =
627
- /<script[^>]*\ssrc=["'][^"']*manifest\.[a-z][\w.-]*\.(?:min\.)?js["'][^>]*>\s*<\/script>/gi;
628
- let out = html.replace(pluginPattern, '');
629
- // Alpine (re-injected by the loader at runtime after plugin registration)
630
- const alpinePattern =
631
- /<script[^>]*\ssrc=["'][^"']*\/alpinejs@[^"']*["'][^>]*>\s*<\/script>/gi;
632
- out = out.replace(alpinePattern, '');
633
- // Runtime library scripts loaded by plugins (yaml parser, markdown, etc.)
634
- const runtimePattern =
635
- /<script[^>]*\ssrc=["'][^"']*(?:papaparse[^"']*\.min\.js|marked[^"']*\.min\.js|highlight[^"']*\.min\.js|js-yaml[^"']*\.min\.js)[^"']*["'][^>]*>\s*<\/script>/gi;
636
- out = out.replace(runtimePattern, '');
637
- 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
+ );
638
652
  }
639
653
 
640
654
  function stripRuntimeTailwindArtifacts(html) {
@@ -1391,7 +1405,7 @@ function generateLocaleVariantHtml({
1391
1405
 
1392
1406
  // Standard Node.js post-processing (same sequence as processPath)
1393
1407
  html = stripDevOnlyContent(html);
1394
- html = stripInjectedPluginScripts(html);
1408
+ html = stripInjectedPluginScripts(html, config.root);
1395
1409
  if (tailwindBuilt) html = stripRuntimeTailwindArtifacts(html);
1396
1410
 
1397
1411
  const pageUtilityBlocks = [];
@@ -1999,22 +2013,6 @@ async function runPrerender(config) {
1999
2013
  // (`hydratePrerenderedPage` in manifest.js) reads the contract and
2000
2014
  // restores source attributes before Alpine starts.
2001
2015
  await page.evaluateOnNewDocument(() => {
2002
- // Snapshot the script srcs that exist in the author's original HTML
2003
- // BEFORE any loader/plugin injects additional scripts. Used later to
2004
- // strip runtime-injected scripts from the serialized output while
2005
- // keeping author-intentional ones (analytics, third-party widgets, etc.).
2006
- window.__manifestOriginalScriptSrcs = new Set();
2007
- const _snapScripts = () => {
2008
- document.querySelectorAll('script[src]').forEach(s => {
2009
- window.__manifestOriginalScriptSrcs.add(s.getAttribute('src'));
2010
- });
2011
- };
2012
- if (document.readyState === 'loading') {
2013
- document.addEventListener('DOMContentLoaded', _snapScripts, { once: true });
2014
- }
2015
- // Also snap immediately for any scripts already parsed
2016
- _snapScripts();
2017
-
2018
2016
  // element -> { attrName: originalValue (null if attribute was absent) }
2019
2017
  // Keyed by reference so detached elements drop out naturally.
2020
2018
  const sourceAttrs = new Map();
@@ -2857,20 +2855,6 @@ async function runPrerender(config) {
2857
2855
  toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
2858
2856
  });
2859
2857
 
2860
- // Remove scripts that were injected at runtime (by the loader, plugins,
2861
- // or third-party libraries) but were NOT in the author's original HTML.
2862
- // This is done in the browser context (before serialization) so the
2863
- // removal is permanent in the captured outerHTML. The approach is
2864
- // future-proof: new plugins and arbitrary third-party scripts are handled
2865
- // automatically without updating a hardcoded allowlist.
2866
- await page.evaluate(() => {
2867
- const originals = window.__manifestOriginalScriptSrcs || new Set();
2868
- document.querySelectorAll('script[src]').forEach(s => {
2869
- const src = s.getAttribute('src');
2870
- if (src && !originals.has(src)) s.remove();
2871
- });
2872
- });
2873
-
2874
2858
  let html = await page.evaluate(() => document.documentElement.outerHTML);
2875
2859
  // Inject the hydration contract blob into the raw HTML *before* caching
2876
2860
  // it for locale variant generation, so every locale variant inherits the
@@ -2897,7 +2881,7 @@ async function runPrerender(config) {
2897
2881
  pushDebug({ path: displayPath, stage: 'pre-serialize', metrics: post });
2898
2882
  }
2899
2883
  html = stripDevOnlyContent(html);
2900
- html = stripInjectedPluginScripts(html);
2884
+ html = stripInjectedPluginScripts(html, config.root);
2901
2885
  if (tailwindBuilt) {
2902
2886
  html = stripRuntimeTailwindArtifacts(html);
2903
2887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {