@web/polyfills-loader
Version:
Generate loader for loading browser polyfills based on feature detection
137 lines (118 loc) • 3.92 kB
text/typescript
import { Document, Node, ParentNode, parse, serialize } from 'parse5';
import {
findElements,
getAttribute,
createScript,
getTextContent,
insertBefore,
appendChild,
createElement,
findElement,
getTagName,
Element,
} from '@web/parse5-utils';
import { PolyfillsLoaderConfig, PolyfillsLoader, GeneratedFile } from './types.js';
import { createPolyfillsLoader } from './createPolyfillsLoader.js';
import { hasFileOfType, fileTypes } from './utils.js';
function injectImportMapPolyfill(headAst: ParentNode, originalScript: Node, type: string) {
const systemJsScript = createScript({ type }, getTextContent(originalScript));
insertBefore(headAst, systemJsScript, originalScript);
}
function findImportMapScripts(document: Document) {
const scripts = findElements(document, script => getAttribute(script, 'type') === 'importmap');
const inline: Node[] = [];
const external: Node[] = [];
for (const script of scripts) {
if (getAttribute(script, 'src')) {
external.push(script);
} else {
inline.push(script);
}
}
return { inline, external };
}
function injectImportMapPolyfills(
documentAst: Document,
headAst: ParentNode,
cfg: PolyfillsLoaderConfig,
) {
const importMapScripts = findImportMapScripts(documentAst);
if (importMapScripts.external.length === 0 && importMapScripts.inline.length === 0) {
return;
}
const polyfillSystemJs = hasFileOfType(cfg, fileTypes.SYSTEMJS);
const importMaps = [...importMapScripts.external, ...importMapScripts.inline];
importMaps.forEach(originalScript => {
if (polyfillSystemJs) {
injectImportMapPolyfill(headAst, originalScript, 'systemjs-importmap');
}
});
}
function injectLoaderScript(
bodyAst: ParentNode,
polyfillsLoader: PolyfillsLoader,
cfg: PolyfillsLoaderConfig,
) {
let loaderScript: Element;
if (cfg.externalLoaderScript) {
const loaderScriptFile = polyfillsLoader.polyfillFiles.find(f => f.path.endsWith('loader.js'));
if (!loaderScriptFile) {
throw new Error('Missing polyfills loader script file');
}
loaderScript = createScript({ src: loaderScriptFile.path });
} else {
loaderScript = createScript({}, polyfillsLoader.code);
}
appendChild(bodyAst, loaderScript);
}
function injectPrefetchLinks(headAst: ParentNode, cfg: PolyfillsLoaderConfig) {
for (const file of cfg.modern!.files) {
const { path } = file;
const href = path.startsWith('.') || path.startsWith('/') ? path : `./${path}`;
if (file.type === fileTypes.MODULE) {
appendChild(
headAst,
createElement('link', {
rel: 'preload',
href,
as: 'script',
crossorigin: 'anonymous',
}),
);
} else {
appendChild(headAst, createElement('link', { rel: 'preload', href, as: 'script' }));
}
}
}
export interface InjectPolyfillsLoaderResult {
htmlString: string;
polyfillFiles: GeneratedFile[];
}
/**
* Transforms an index.html file, injecting a polyfills loader for
* compatibility with older browsers.
*/
export async function injectPolyfillsLoader(
htmlString: string,
cfg: PolyfillsLoaderConfig,
): Promise<InjectPolyfillsLoaderResult> {
const documentAst = parse(htmlString);
const headAst = findElement(documentAst, e => getTagName(e) === 'head');
const bodyAst = findElement(documentAst, e => getTagName(e) === 'body');
if (!headAst || !bodyAst) {
throw new Error(`Invalid index.html: missing <head> or <body>`);
}
const polyfillsLoader = await createPolyfillsLoader(cfg);
if (polyfillsLoader === null) {
return { htmlString, polyfillFiles: [] };
}
if (cfg.preload) {
injectPrefetchLinks(headAst, cfg);
}
injectImportMapPolyfills(documentAst, headAst, cfg);
injectLoaderScript(bodyAst, polyfillsLoader, cfg);
return {
htmlString: serialize(documentAst),
polyfillFiles: polyfillsLoader.polyfillFiles,
};
}