Files
memind/mindspace-published-script-localize.mjs
T
john 1ec971515b
Memind CI / Test, build, and release guards (push) Failing after 3m57s
feat(publish): auto-localize Leaflet CDN to platform assets
Ship Leaflet 1.9.4 under public/assets/leaflet and rewrite common CDN script/stylesheet URLs during publish and public HTML delivery, matching the existing Chart.js guard pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 16:05:50 +08:00

142 lines
4.5 KiB
JavaScript

const SCRIPT_SRC_PATTERN = /<script\b([^>]*)\bsrc\s*=\s*(['"])([^'"]+)\2([^>]*)>/gi;
const STYLESHEET_HREF_PATTERN =
/<link\b([^>]*)\brel\s*=\s*(['"])stylesheet\2([^>]*)\bhref\s*=\s*(['"])([^'"]+)\4([^>]*)>/gi;
const STYLESHEET_HREF_ALT_PATTERN =
/<link\b([^>]*)\bhref\s*=\s*(['"])([^'"]+)\2([^>]*)\brel\s*=\s*(['"])stylesheet\5([^>]*)>/gi;
export const KNOWN_PLATFORM_SCRIPT_ALIASES = [
{
id: 'chart.js',
target: '/assets/chart.umd.min.js',
patterns: [
/cdn\.jsdelivr\.net\/npm\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js/i,
/cdnjs\.cloudflare\.com\/ajax\/libs\/Chart\.js\/[^/]+\/chart\.(?:umd\.)?min\.js/i,
/unpkg\.com\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js/i,
],
},
{
id: 'leaflet',
target: '/assets/leaflet/leaflet.js',
patterns: [
/cdn\.jsdelivr\.net\/npm\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js/i,
/cdnjs\.cloudflare\.com\/ajax\/libs\/leaflet\/[^/]+\/leaflet\.js/i,
/unpkg\.com\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js/i,
],
},
];
export const KNOWN_PLATFORM_STYLESHEET_ALIASES = [
{
id: 'leaflet',
target: '/assets/leaflet/leaflet.css',
patterns: [
/cdn\.jsdelivr\.net\/npm\/leaflet(?:@[^/]+)?\/dist\/leaflet\.css/i,
/cdnjs\.cloudflare\.com\/ajax\/libs\/leaflet\/[^/]+\/leaflet\.css/i,
/unpkg\.com\/leaflet(?:@[^/]+)?\/dist\/leaflet\.css/i,
],
},
];
function normalizeScriptUrl(value) {
return String(value ?? '').trim().replace(/&amp;/g, '&');
}
export function isExternalScriptSrc(src) {
const normalized = normalizeScriptUrl(src);
if (!normalized) return false;
if (/^https?:\/\//i.test(normalized)) return true;
if (/^\/\//.test(normalized)) return true;
return false;
}
function resolveKnownPlatformTarget(src, aliases) {
const normalized = normalizeScriptUrl(src);
if (!isExternalScriptSrc(normalized)) return null;
const probe = normalized.startsWith('//') ? `https:${normalized}` : normalized;
for (const alias of aliases) {
if (alias.patterns.some((pattern) => pattern.test(probe))) {
return alias.target;
}
}
return null;
}
function resolveKnownPlatformScriptTarget(src) {
return resolveKnownPlatformTarget(src, KNOWN_PLATFORM_SCRIPT_ALIASES);
}
function resolveKnownPlatformStylesheetTarget(href) {
return resolveKnownPlatformTarget(href, KNOWN_PLATFORM_STYLESHEET_ALIASES);
}
export function findExternalScriptSources(html) {
const sources = [];
const seen = new Set();
for (const match of String(html ?? '').matchAll(SCRIPT_SRC_PATTERN)) {
const src = normalizeScriptUrl(match[3]);
if (!isExternalScriptSrc(src) || seen.has(src)) continue;
seen.add(src);
sources.push(src);
}
return sources;
}
export function rewriteKnownCdnScriptSources(html) {
const rewrites = [];
const next = String(html ?? '').replace(
SCRIPT_SRC_PATTERN,
(full, before, quote, src, after) => {
const normalized = normalizeScriptUrl(src);
const target = resolveKnownPlatformScriptTarget(normalized);
if (!target) return full;
rewrites.push({ from: normalized, to: target });
return `<script${before}src=${quote}${target}${quote}${after}>`;
},
);
return {
html: next,
rewrittenCount: rewrites.length,
rewrites,
};
}
export function rewriteKnownCdnStylesheetSources(html) {
const rewrites = [];
let next = String(html ?? '');
const replaceStylesheet = (full, before, quote, href, after) => {
const normalized = normalizeScriptUrl(href);
const target = resolveKnownPlatformStylesheetTarget(normalized);
if (!target) return full;
rewrites.push({ from: normalized, to: target });
return `<link${before}href=${quote}${target}${quote}${after}>`;
};
next = next.replace(
STYLESHEET_HREF_PATTERN,
(full, before, _relQuote, _middle, hrefQuote, href, after) =>
replaceStylesheet(full, before, hrefQuote, href, after),
);
next = next.replace(
STYLESHEET_HREF_ALT_PATTERN,
(full, before, hrefQuote, href, _middle, _relQuote, after) =>
replaceStylesheet(full, before, hrefQuote, href, after),
);
return {
html: next,
rewrittenCount: rewrites.length,
rewrites,
};
}
export function rewriteKnownCdnAssetSources(html) {
const scripts = rewriteKnownCdnScriptSources(html);
const stylesheets = rewriteKnownCdnStylesheetSources(scripts.html);
return {
html: stylesheets.html,
rewrittenCount: scripts.rewrittenCount + stylesheets.rewrittenCount,
rewrites: [...scripts.rewrites, ...stylesheets.rewrites],
};
}