@web/polyfills-loader
Version:
Generate loader for loading browser polyfills based on feature detection
233 lines (231 loc) • 8.31 kB
JavaScript
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
;