263 lines
7.7 KiB
TypeScript
263 lines
7.7 KiB
TypeScript
import {
|
|
PolyfillsLoaderConfig,
|
|
File,
|
|
GeneratedFile,
|
|
PolyfillFile,
|
|
PolyfillsLoader,
|
|
LegacyEntrypoint,
|
|
} from './types';
|
|
|
|
import { transformAsync } from '@babel/core';
|
|
import Terser from 'terser';
|
|
import { fileTypes, hasFileOfType, cleanImportPath } from './utils';
|
|
import { createPolyfillsData } from './createPolyfillsData';
|
|
import path from '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');
|
|
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: PolyfillsLoaderConfig, polyfills: PolyfillFile[]) {
|
|
const { MODULE, SCRIPT } = fileTypes;
|
|
if (
|
|
(polyfills && polyfills.length > 0) ||
|
|
[SCRIPT, MODULE].some(type => hasFileOfType(cfg, type))
|
|
) {
|
|
return loadScriptFunction;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Returns a js statement which loads the given resource in the browser.
|
|
*/
|
|
function createLoadFile(file: File) {
|
|
const resourcePath = cleanImportPath(file.path);
|
|
const attributesAsJsCodeString = file.attributes ? JSON.stringify(file.attributes) : '[]';
|
|
|
|
switch (file.type) {
|
|
case fileTypes.SCRIPT:
|
|
return `loadScript('${resourcePath}', null, ${attributesAsJsCodeString})`;
|
|
case fileTypes.MODULE:
|
|
return `loadScript('${resourcePath}', 'module', ${attributesAsJsCodeString})`;
|
|
case fileTypes.MODULESHIM:
|
|
return `loadScript('${resourcePath}', 'module-shim', ${attributesAsJsCodeString})`;
|
|
case 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: File[]) {
|
|
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: PolyfillsLoaderConfig) {
|
|
const loadResources = cfg.modern && cfg.modern.files ? createLoadFiles(cfg.modern.files) : '';
|
|
if (!cfg.legacy || cfg.legacy.length === 0) {
|
|
return loadResources;
|
|
}
|
|
|
|
function reduceFn(all: string, current: LegacyEntrypoint, i: number) {
|
|
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: PolyfillsLoaderConfig, polyfills: PolyfillFile[]) {
|
|
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: string, cfg: PolyfillsLoaderConfig) {
|
|
const relativePath = path.join(cfg.relativePathToPolyfills || './', polyfillPath);
|
|
return relativePath.split(path.sep).join(path.posix.sep);
|
|
}
|
|
|
|
/**
|
|
* Creates code which loads the configured polyfills
|
|
*/
|
|
function createPolyfillsLoaderCode(
|
|
cfg: PolyfillsLoaderConfig,
|
|
polyfills: PolyfillFile[],
|
|
): { loadPolyfillsCode: string; generatedFiles: GeneratedFile[] } {
|
|
if (!polyfills || polyfills.length === 0) {
|
|
return { loadPolyfillsCode: '', generatedFiles: [] };
|
|
}
|
|
const generatedFiles: GeneratedFile[] = [];
|
|
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.).
|
|
*/
|
|
export async function createPolyfillsLoader(
|
|
cfg: PolyfillsLoaderConfig,
|
|
): Promise<PolyfillsLoader | null> {
|
|
let polyfillFiles = await 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: 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');
|
|
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 Terser.minify(code);
|
|
if (!output || !output.code) {
|
|
throw new Error('Could not minify loader.');
|
|
}
|
|
({ code } = output);
|
|
} else {
|
|
const output = await 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 };
|
|
}
|