UNPKG

@web/polyfills-loader

Version:

Generate loader for loading browser polyfills based on feature detection

137 lines (118 loc) 3.92 kB
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, }; }