mnfst-render 0.5.15 → 0.5.17
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 +80 -64
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -609,32 +609,46 @@ function stripDevOnlyContent(html) {
|
|
|
609
609
|
return out;
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
// --- Strip
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 = [];
|
|
@@ -1460,6 +1474,26 @@ function loadContentForPrerender(manifest, rootDir, locale) {
|
|
|
1460
1474
|
content = parseCsvToKeyValue(join(rootDir, data.slice(1)), loc);
|
|
1461
1475
|
} else if (data && typeof data === 'object' && data.locales && typeof data.locales === 'string') {
|
|
1462
1476
|
content = parseCsvToKeyValue(join(rootDir, data.locales.slice(1)), loc);
|
|
1477
|
+
} else if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
1478
|
+
// Per-locale files: { "en": "/data/content.en.yaml", "fr": "/data/content.fr.yaml", ... }
|
|
1479
|
+
const filePath = data[loc] || data[Object.keys(data)[0]];
|
|
1480
|
+
if (typeof filePath === 'string') {
|
|
1481
|
+
const fullPath = join(rootDir, filePath.startsWith('/') ? filePath.slice(1) : filePath);
|
|
1482
|
+
if (existsSync(fullPath)) {
|
|
1483
|
+
try {
|
|
1484
|
+
const raw = readFileSync(fullPath, 'utf8');
|
|
1485
|
+
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
|
|
1486
|
+
let jsYaml = null;
|
|
1487
|
+
try { jsYaml = require('js-yaml'); } catch { /* skip */ }
|
|
1488
|
+
if (jsYaml) content = jsYaml.load(raw) || {};
|
|
1489
|
+
} else if (filePath.endsWith('.json')) {
|
|
1490
|
+
content = JSON.parse(raw);
|
|
1491
|
+
} else if (filePath.endsWith('.csv')) {
|
|
1492
|
+
content = parseCsvToKeyValue(fullPath, loc);
|
|
1493
|
+
}
|
|
1494
|
+
} catch { /* ignore parse errors */ }
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1463
1497
|
}
|
|
1464
1498
|
if (manifest.description !== undefined && content.description === undefined) {
|
|
1465
1499
|
content.description = manifest.description;
|
|
@@ -1532,15 +1566,27 @@ function resolveHeadXBindings(html, xData) {
|
|
|
1532
1566
|
.replace(/>/g, '>')
|
|
1533
1567
|
.replace(/"/g, '"');
|
|
1534
1568
|
return html.replace(/<head>([\s\S]*?)<\/head>/i, (_, headContent) => {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1569
|
+
// Process each tag in <head> that has a :attr or x-bind:attr binding
|
|
1570
|
+
const out = headContent.replace(/<[^>]+>/g, (tag) => {
|
|
1571
|
+
// Find all :attr="$x...." or x-bind:attr="$x...." bindings in this tag
|
|
1572
|
+
const bindingRe = /\s(?::|x-bind:)(\w+)=["'](\$x\.[^"']+)["']/g;
|
|
1573
|
+
let m;
|
|
1574
|
+
let newTag = tag;
|
|
1575
|
+
while ((m = bindingRe.exec(tag)) !== null) {
|
|
1576
|
+
const attr = m[1];
|
|
1577
|
+
const expr = m[2];
|
|
1538
1578
|
const path = expr.replace(/^\$x\./, '').trim();
|
|
1539
1579
|
const value = getXPath(xData, path);
|
|
1540
|
-
if (value === undefined)
|
|
1541
|
-
|
|
1580
|
+
if (value === undefined) continue;
|
|
1581
|
+
// Remove the binding
|
|
1582
|
+
newTag = newTag.replace(m[0], '');
|
|
1583
|
+
// Remove existing static fallback for this attr
|
|
1584
|
+
newTag = newTag.replace(new RegExp(`\\s${attr}=["'][^"']*["']`), '');
|
|
1585
|
+
// Insert the resolved attr before the closing >
|
|
1586
|
+
newTag = newTag.replace(/>$/, ` ${attr}="${esc(value)}">`);
|
|
1542
1587
|
}
|
|
1543
|
-
|
|
1588
|
+
return newTag;
|
|
1589
|
+
});
|
|
1544
1590
|
return `<head>${out}</head>`;
|
|
1545
1591
|
});
|
|
1546
1592
|
}
|
|
@@ -1999,22 +2045,6 @@ async function runPrerender(config) {
|
|
|
1999
2045
|
// (`hydratePrerenderedPage` in manifest.js) reads the contract and
|
|
2000
2046
|
// restores source attributes before Alpine starts.
|
|
2001
2047
|
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
2048
|
// element -> { attrName: originalValue (null if attribute was absent) }
|
|
2019
2049
|
// Keyed by reference so detached elements drop out naturally.
|
|
2020
2050
|
const sourceAttrs = new Map();
|
|
@@ -2857,20 +2887,6 @@ async function runPrerender(config) {
|
|
|
2857
2887
|
toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
|
|
2858
2888
|
});
|
|
2859
2889
|
|
|
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
2890
|
let html = await page.evaluate(() => document.documentElement.outerHTML);
|
|
2875
2891
|
// Inject the hydration contract blob into the raw HTML *before* caching
|
|
2876
2892
|
// it for locale variant generation, so every locale variant inherits the
|
|
@@ -2897,7 +2913,7 @@ async function runPrerender(config) {
|
|
|
2897
2913
|
pushDebug({ path: displayPath, stage: 'pre-serialize', metrics: post });
|
|
2898
2914
|
}
|
|
2899
2915
|
html = stripDevOnlyContent(html);
|
|
2900
|
-
html = stripInjectedPluginScripts(html);
|
|
2916
|
+
html = stripInjectedPluginScripts(html, config.root);
|
|
2901
2917
|
if (tailwindBuilt) {
|
|
2902
2918
|
html = stripRuntimeTailwindArtifacts(html);
|
|
2903
2919
|
}
|