UNPKG

@web/polyfills-loader

Version:

Generate loader for loading browser polyfills based on feature detection

254 lines (222 loc) 8.97 kB
import path from 'path'; import fs from 'fs'; import { minify } from 'terser'; import { PolyfillsLoaderConfig, PolyfillConfig, PolyfillFile } from './types.js'; import { createContentHash, noModuleSupportTest, hasFileOfType, fileTypes } from './utils.js'; export async function createPolyfillsData(cfg: PolyfillsLoaderConfig): Promise<PolyfillFile[]> { const { polyfills = {} } = cfg; const polyfillConfigs: PolyfillConfig[] = []; function addPolyfillConfig(polyfillConfig: PolyfillConfig) { try { polyfillConfigs.push(polyfillConfig); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { throw new Error( `[Polyfills loader]: Error resolving polyfill ${polyfillConfig.name}` + ' Are dependencies installed correctly?', ); } throw error; } } if (polyfills.coreJs) { addPolyfillConfig({ name: 'core-js', path: require.resolve('core-js-bundle/minified.js'), test: noModuleSupportTest, }); } if (polyfills.URLPattern) { addPolyfillConfig({ name: 'urlpattern-polyfill', test: '"URLPattern" in window', path: require.resolve('urlpattern-polyfill'), }); } if (polyfills.esModuleShims) { addPolyfillConfig({ name: 'es-module-shims', test: polyfills.esModuleShims !== 'always' ? '1' : undefined, path: require.resolve('es-module-shims'), minify: true, }); } if (polyfills.constructibleStylesheets) { addPolyfillConfig({ name: 'constructible-style-sheets-polyfill', test: '!("adoptedStyleSheets" in document)', path: require.resolve('construct-style-sheets-polyfill'), }); } if (polyfills.regeneratorRuntime) { addPolyfillConfig({ name: 'regenerator-runtime', test: polyfills.regeneratorRuntime !== 'always' ? noModuleSupportTest : undefined, path: require.resolve('regenerator-runtime/runtime'), }); } if (polyfills.fetch) { addPolyfillConfig({ name: 'fetch', test: `!('fetch' in window)${ polyfills.abortController ? " || !('Request' in window) || !('signal' in window.Request.prototype)" : '' }`, path: polyfills.abortController ? [ require.resolve('whatwg-fetch/dist/fetch.umd.js'), require.resolve('abortcontroller-polyfill/dist/umd-polyfill.js'), ] : [require.resolve('whatwg-fetch/dist/fetch.umd.js')], minify: true, }); } if (polyfills.abortController && !polyfills.fetch) { throw new Error('Cannot polyfill AbortController without fetch.'); } // load systemjs, an es module polyfill, if one of the entries needs it const hasSystemJs = cfg.polyfills && cfg.polyfills.custom && cfg.polyfills.custom.find(c => c.name === 'systemjs'); if ( polyfills.systemjs || polyfills.systemjsExtended || (!hasSystemJs && hasFileOfType(cfg, fileTypes.SYSTEMJS)) ) { const name = 'systemjs'; const alwaysLoad = cfg.modern && cfg.modern.files && cfg.modern.files.some(f => f.type === fileTypes.SYSTEMJS); const test = alwaysLoad || !cfg.legacy ? undefined : cfg.legacy.map(e => e.test).join(' || '); if (polyfills.systemjsExtended) { // full systemjs, including import maps polyfill addPolyfillConfig({ name, test, path: require.resolve('systemjs/dist/system.min.js'), }); } else { // plain systemjs as es module polyfill addPolyfillConfig({ name, test, path: require.resolve('systemjs/dist/s.min.js'), }); } } if (polyfills.dynamicImport) { addPolyfillConfig({ name: 'dynamic-import', /** * dynamic import is syntax, not an actual function so we cannot feature detect it without using an import statement. * using a dynamic import on a browser which doesn't support it throws a syntax error and prevents the entire script * from being run, so we need to dynamically create and execute a function and catch the error. * * CSP can block the dynamic function, in which case the polyfill will always be loaded which is ok. The polyfill itself * uses Blob, which might be blocked by CSP as well. In that case users should use systemjs instead. */ test: "'noModule' in HTMLScriptElement.prototype && " + "(function () { try { Function('window.importShim = s => import(s);').call(); return false; } catch (_) { return true; } })()", path: require.resolve('dynamic-import-polyfill/dist/dynamic-import-polyfill.umd.js'), initializer: "window.dynamicImportPolyfill.initialize({ importFunctionName: 'importShim' });", }); } if (polyfills.intersectionObserver) { addPolyfillConfig({ name: 'intersection-observer', test: "!('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype)", path: require.resolve('intersection-observer/intersection-observer.js'), minify: true, }); } if (polyfills.resizeObserver) { addPolyfillConfig({ name: 'resize-observer', test: "!('ResizeObserver' in window)", path: require.resolve('resize-observer-polyfill/dist/ResizeObserver.global.js'), minify: true, }); } if (polyfills.scopedCustomElementRegistry) { addPolyfillConfig({ name: 'scoped-custom-element-registry', test: "!('createElement' in ShadowRoot.prototype)", path: require.resolve( '@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js', ), }); } if (polyfills.webcomponents && !polyfills.shadyCssCustomStyle) { addPolyfillConfig({ name: 'webcomponents', test: "!('attachShadow' in Element.prototype) || !('getRootNode' in Element.prototype) || (window.ShadyDOM && window.ShadyDOM.force)", path: require.resolve('@webcomponents/webcomponentsjs/webcomponents-bundle.js'), }); // If a browser does not support nomodule attribute, but does support custom elements, we need // to load the custom elements es5 adapter. This is the case for Safari 10.1 addPolyfillConfig({ name: 'custom-elements-es5-adapter', test: "!('noModule' in HTMLScriptElement.prototype) && 'getRootNode' in Element.prototype", path: require.resolve('@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'), }); } if (polyfills.webcomponents && polyfills.shadyCssCustomStyle) { // shadycss/custom-style-interface polyfill *must* load after the webcomponents polyfill or it doesn't work. // to get around that, concat the two together. addPolyfillConfig({ name: 'webcomponents-shady-css-custom-style', test: "!('attachShadow' in Element.prototype) || !('getRootNode' in Element.prototype)", path: [ require.resolve('@webcomponents/webcomponentsjs/webcomponents-bundle.js'), require.resolve('@webcomponents/shadycss/custom-style-interface.min.js'), require.resolve('shady-css-scoped-element/shady-css-scoped-element.min.js'), ], }); } polyfillConfigs.push(...(polyfills.custom || [])); function readPolyfillFileContents(filePath: string): string { const codePath = path.resolve(filePath); if (!codePath || !fs.existsSync(codePath) || !fs.statSync(codePath).isFile()) { throw new Error(`Could not find a file at ${filePath}`); } const contentLines = fs.readFileSync(filePath, 'utf-8').split('\n'); // remove source map url for (let i = contentLines.length - 1; i >= 0; i -= 1) { if (contentLines[i].startsWith('//# sourceMappingURL')) { contentLines[i] = ''; } } return contentLines.join('\n'); } const polyfillFiles: PolyfillFile[] = []; for (const polyfillConfig of polyfillConfigs) { if (!polyfillConfig.name || !polyfillConfig.path) { throw new Error(`A polyfill should have a name and a path property.`); } let content = ''; if (Array.isArray(polyfillConfig.path)) { content = polyfillConfig.path.map(p => readPolyfillFileContents(p)).join(''); } else { content = readPolyfillFileContents(polyfillConfig.path); } if (polyfillConfig.minify) { const minifyResult = await minify(content, { sourceMap: false }); // @ts-ignore content = minifyResult.code; } const filePath = `${path.posix.join( cfg.polyfillsDir || 'polyfills', `${polyfillConfig.name}${polyfills.hash !== false ? `.${createContentHash(content)}` : ''}`, )}.js`; const polyfillFile = { name: polyfillConfig.name, type: polyfillConfig.fileType || fileTypes.SCRIPT, path: filePath, content, test: polyfillConfig.test, initializer: polyfillConfig.initializer, }; polyfillFiles.push(polyfillFile); } return polyfillFiles; }