UNPKG

@web/polyfills-loader

Version:

Generate loader for loading browser polyfills based on feature detection

233 lines (231 loc) 8.31 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createPolyfillsLoader = void 0; const core_1 = require("@babel/core"); const terser_1 = require("terser"); const utils_js_1 = require("./utils.js"); const createPolyfillsData_js_1 = require("./createPolyfillsData.js"); const path_1 = __importDefault(require("path")); /** * Function which loads a script dynamically, returning a thenable (object with then function) * because Promise might not be loaded yet */ const loadScriptFunction = ` function loadScript(src, type, attributes) { return new Promise(function (resolve) { var script = document.createElement('script'); script.fetchPriority = 'high'; function onLoaded() { if (script.parentElement) { script.parentElement.removeChild(script); } resolve(); } script.src = src; script.onload = onLoaded; if (attributes) { attributes.forEach(function (att) { script.setAttribute(att.name, att.value); }); } script.onerror = function () { console.error('[polyfills-loader] failed to load: ' + src + ' check the network tab for HTTP status.'); onLoaded(); } if (type) script.type = type; document.head.appendChild(script); }); } `; /** * Returns the loadScriptFunction if a script will be loaded for this config. */ function createLoadScriptCode(cfg, polyfills) { const { MODULE, SCRIPT } = utils_js_1.fileTypes; if ((polyfills && polyfills.length > 0) || [SCRIPT, MODULE].some(type => (0, utils_js_1.hasFileOfType)(cfg, type))) { return loadScriptFunction; } return ''; } /** * Returns a js statement which loads the given resource in the browser. */ function createLoadFile(file) { const resourcePath = (0, utils_js_1.cleanImportPath)(file.path); const attributesAsJsCodeString = file.attributes ? JSON.stringify(file.attributes) : '[]'; switch (file.type) { case utils_js_1.fileTypes.SCRIPT: return `loadScript('${resourcePath}', null, ${attributesAsJsCodeString})`; case utils_js_1.fileTypes.MODULE: return `loadScript('${resourcePath}', 'module', ${attributesAsJsCodeString})`; case utils_js_1.fileTypes.MODULESHIM: return `loadScript('${resourcePath}', 'module-shim', ${attributesAsJsCodeString})`; case utils_js_1.fileTypes.SYSTEMJS: return `System.import('${resourcePath}')`; default: throw new Error(`Unknown resource type: ${file.type}`); } } /** * Creates a statement which loads the given resources in the browser sequentially. */ function createLoadFiles(files) { if (files.length === 1) { return createLoadFile(files[0]); } return `[ ${files.map(r => `function() { return ${createLoadFile(r)} }`)} ].reduce(function (a, c) { return a.then(c); }, Promise.resolve())`; } /** * Creates js code which loads the correct resources, uses runtime feature detection * of legacy resources are configured to load the appropriate resources. */ function createLoadFilesFunction(cfg) { const loadResources = cfg.modern && cfg.modern.files ? createLoadFiles(cfg.modern.files) : ''; if (!cfg.legacy || cfg.legacy.length === 0) { return loadResources; } function reduceFn(all, current, i) { return `${all}${i !== 0 ? ' else ' : ''}if (${current.test}) { ${createLoadFiles(current.files)} }`; } const loadLegacyResources = cfg.legacy.reduce(reduceFn, ''); return `${loadLegacyResources} else { ${loadResources} }`; } /** * Creates js code which waits for polyfills if applicable, and executes * the code which loads entrypoints. */ function createLoadFilesCode(cfg, polyfills) { const loadFilesFunction = createLoadFilesFunction(cfg); // create a separate loadFiles to be run after polyfills if (polyfills && polyfills.length > 0) { return ` function loadFiles() { ${loadFilesFunction} } if (polyfills.length) { Promise.all(polyfills).then(loadFiles); } else { loadFiles(); }`; } // there are no polyfills, load entries straight away return `${loadFilesFunction}`; } /** * Returns the relative path to a polyfill (in posix path format suitable for * a relative URL) given the plugin configuation */ function relativePolyfillPath(polyfillPath, cfg) { const relativePath = path_1.default.join(cfg.relativePathToPolyfills || './', polyfillPath); return relativePath.split(path_1.default.sep).join(path_1.default.posix.sep); } /** * Creates code which loads the configured polyfills */ function createPolyfillsLoaderCode(cfg, polyfills) { if (!polyfills || polyfills.length === 0) { return { loadPolyfillsCode: '', generatedFiles: [] }; } const generatedFiles = []; let loadPolyfillsCode = ' var polyfills = [];'; polyfills.forEach(polyfill => { let loadScript = `loadScript('./${relativePolyfillPath(polyfill.path, cfg)}')`; if (polyfill.initializer) { loadScript += `.then(function () { ${polyfill.initializer} })`; } const loadPolyfillCode = `polyfills.push(${loadScript});`; if (polyfill.test) { loadPolyfillsCode += `if (${polyfill.test}) { ${loadPolyfillCode} }`; } else { loadPolyfillsCode += `${loadPolyfillCode}`; } generatedFiles.push({ type: polyfill.type, path: polyfill.path, content: polyfill.content, }); }); return { loadPolyfillsCode, generatedFiles }; } /** * Creates a loader script that executes immediately, loading the configured * polyfills and resources (app entrypoints, scripts etc.). */ async function createPolyfillsLoader(cfg) { let polyfillFiles = await (0, createPolyfillsData_js_1.createPolyfillsData)(cfg); const coreJs = polyfillFiles.find(pf => pf.name === 'core-js'); polyfillFiles = polyfillFiles.filter(pf => pf !== coreJs); const { loadPolyfillsCode, generatedFiles } = createPolyfillsLoaderCode(cfg, polyfillFiles); let code = ` ${createLoadScriptCode(cfg, polyfillFiles)} ${loadPolyfillsCode} ${createLoadFilesCode(cfg, polyfillFiles)} `; if (coreJs) { generatedFiles.push({ type: utils_js_1.fileTypes.SCRIPT, path: coreJs.path, content: coreJs.content, }); // if core-js should be polyfilled, load it first and then the rest because most // polyfills rely on things like Promise to be already loaded code = `(function () { function polyfillsLoader() { ${code} } if (${coreJs.test}) { var s = document.createElement('script'); s.fetchPriority = 'high'; function onLoaded() { document.head.removeChild(s); polyfillsLoader(); } s.src = "./${relativePolyfillPath(coreJs.path, cfg)}"; s.onload = onLoaded; s.onerror = function () { console.error('[polyfills-loader] failed to load: ' + s.src + ' check the network tab for HTTP status.'); onLoaded(); } document.head.appendChild(s); } else { polyfillsLoader(); } })();`; } else { code = `(function () { ${code} })();`; } if (cfg.minify) { const output = await (0, terser_1.minify)(code); if (!output || !output.code) { throw new Error('Could not minify loader.'); } ({ code } = output); } else { const output = await (0, core_1.transformAsync)(code, { babelrc: false, configFile: false }); if (!output || !output.code) { throw new Error('Could not prettify loader.'); } ({ code } = output); } if (cfg.externalLoaderScript) { generatedFiles.push({ type: 'script', path: 'loader.js', content: code }); } return { code, polyfillFiles: generatedFiles }; } exports.createPolyfillsLoader = createPolyfillsLoader; //# sourceMappingURL=createPolyfillsLoader.js.map