Files
espurna/code/html/inline.mjs
Maxim Prokhorov b41a20aa86 webui(build): support type=...;base64
in case there are any unquotable or binary sources

SCRIPT is an odd one per the spec, ignore type=...
as it is describing how minification & bundling is done
2025-04-15 20:43:01 +03:00

184 lines
4.9 KiB
JavaScript

/** @import { Modules } from './preset.mjs' */
/** @import { JSDOM } from 'jsdom' */
/**
* @param {JSDOM} dom
* @param {Modules} modules
*/
export function stripModules(dom, modules) {
let changed = false;
for (const elem of dom.window.document.querySelectorAll('[class*=module-]')) {
const classModules = parseClassModules(
Array.from(elem.classList));
for (const module of classModules) {
if (!modules[module]) {
elem.classList.remove(`module-${module}`);
classModules.delete(module);
changed = true;
}
}
if (classModules.size === 0) {
elem.parentElement?.removeChild(elem);
changed = true;
}
}
return changed;
}
/**
* @param {string[]} classList
* @returns {Set<string>}
*/
export function parseClassModules(classList) {
const classModules = new Set(
classList
.filter((x) => x.startsWith('module-'))
.map((x) => x.replace('module-', '')));
return classModules;
}
/**
* @param {Element} elem
* @param {Modules} modules
*/
export function needElement(elem, modules) {
const classModules = parseClassModules(
Array.from(elem.classList));
if (classModules.size === 0) {
return true;
}
for (const module of classModules) {
if (!modules[module]) {
elem.classList.remove(`module-${module}`);
classModules.delete(module);
}
}
return classModules.size > 0;
}
/** @typedef InlineOptions
* @property {function(string, string, string): (Promise<string> | string)} [resolve]
* process raw input src=..., generate a valid path for the load(...)
* @property {function(string, string, string): (Promise<string> | string)} [load]
* process fs path from src=... or load(...) and return the 'code' to-be injected into the resulting element
* @property {function(string): (void | Promise<void>)} [post]
* execute some action when code was successfuly loaded into the dom
*/
/**
* @param {JSDOM} dom
* @param {Element} elem
* @param {InlineOptions} options
*/
export async function maybeInline(dom, elem, {resolve, load, post} = {}) {
let attr = '';
let src = '';
let tag = '';
let type = elem.getAttribute('type') ?? '';
switch (elem.tagName) {
case 'LINK':
attr = 'href';
if (elem.getAttribute('rel') === 'stylesheet') {
tag = 'STYLE';
} else {
tag = elem.tagName;
}
src = elem.getAttribute(attr) ?? '';
break;
case 'SCRIPT':
attr = 'src';
tag = elem.tagName;
src = elem.getAttribute(attr) ?? '';
break;
}
if (tag && src) {
if (src.startsWith('data:')) {
return;
}
if (src.startsWith('/')) {
src = src.slice(1);
}
if (!load) {
return;
}
let resolved = src;
if (resolve) {
resolved = await Promise.resolve(resolve(src, tag, type));
}
const code = await Promise.resolve(load(resolved, tag, type));
if (!code) {
return;
}
switch (tag) {
case 'LINK':
if (elem.getAttribute('rel') !== 'icon') {
return;
}
if (!code.startsWith('data:')) {
return;
}
elem.setAttribute(attr, code);
break;
case 'STYLE':
const style = dom.window.document.createElement(tag);
style.innerHTML = code;
elem.parentElement?.replaceChild(style, elem);
break;
case 'SCRIPT':
elem.removeAttribute('crossorigin');
elem.removeAttribute(attr);
elem.innerHTML = code;
break;
default:
return;
}
if (post) {
await Promise.resolve(post(resolved));
}
}
}
/**
*
* ref. https://github.com/evanw/esbuild/issues/1895
* from our side, html/src/*.mjs (with the exception of index.mjs) require 'init()' call to be actually set up and used
* as the result, no code from the module should be bundled into the output when module was not initialized
* however, since light module depends on iro.js and does not have `sideEffects: false` in package.json, it would still get bundled because of top-level import
* (...and since module modifying something in global scope is not unheard of...)
* @returns {import('esbuild').Plugin}
*/
export function forceNoSideEffects() {
return {
name: 'no-side-effects',
setup(build) {
build.onResolve({filter: /@jaames\/iro/, namespace: 'file'},
async ({path, ...options}) => {
const result = await build.resolve(path, {...options, namespace: 'noRecurse'});
return {...result, sideEffects: false};
});
},
};
}