/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Path, getSystemPath, logging, normalize, resolve, workspaces } from '@angular-devkit/core'; import { Rule } from '@angular-devkit/schematics'; import { getWorkspace, updateWorkspace } from '../../utility/workspace'; import { Builders, ProjectType } from '../../utility/workspace-models'; export default function (): Rule { return async (host, context) => { const workspace = await getWorkspace(host); for (const [projectName, project] of workspace.projects) { if (project.extensions.projectType !== ProjectType.Application) { // Only interested in application projects continue; } for (const [, target] of project.targets) { // Only interested in Angular Devkit Browser builder if (target?.builder !== Builders.Browser) { continue; } const isES5Needed = await isES5SupportNeeded( resolve( normalize(host.root.path), normalize(project.root), ), ); // Check options if (target.options) { target.options = removeE5BrowserSupportOption(projectName, target.options, isES5Needed, context.logger); } // Go through each configuration entry if (!target.configurations) { continue; } for (const [configurationName, options] of Object.entries(target.configurations)) { target.configurations[configurationName] = removeE5BrowserSupportOption( projectName, options, isES5Needed, context.logger, configurationName, ); } } } return updateWorkspace(workspace); }; } type TargetOptions = workspaces.TargetDefinition['options']; function removeE5BrowserSupportOption( projectName: string, options: TargetOptions, isES5Needed: boolean | undefined, logger: logging.LoggerApi, configurationName = '', ): TargetOptions { if (typeof options?.es5BrowserSupport !== 'boolean') { return options; } const configurationPath = configurationName ? `configurations.${configurationName}.` : ''; if (options.es5BrowserSupport && isES5Needed === false) { logger.warn( `Project '${projectName}' doesn't require ES5 support, but '${configurationPath}es5BrowserSupport' was set to 'true'.\n` + `ES5 polyfills will no longer be added when building this project${configurationName ? ` with '${configurationName}' configuration.` : '.'}\n` + `If ES5 polyfills are needed, add the supported ES5 browsers in the browserslist configuration.`, ); } else if (!options.es5BrowserSupport && isES5Needed === true) { logger.warn( `Project '${projectName}' requires ES5 support, but '${configurationPath}es5BrowserSupport' was set to 'false'.\n` + `ES5 polyfills will be added when building this project${configurationName ? ` with '${configurationName}' configuration.` : '.'}\n` + `If ES5 polyfills are not needed, remove the unsupported ES5 browsers from the browserslist configuration.`, ); } return { ...options, es5BrowserSupport: undefined, }; } /** * True, when one or more browsers requires ES5 support */ async function isES5SupportNeeded(projectRoot: Path): Promise { // y: feature is fully available // n: feature is unavailable // a: feature is partially supported // x: feature is prefixed const criteria = [ 'y', 'a', ]; try { // tslint:disable-next-line:no-implicit-dependencies const browserslist = await import('browserslist'); const supportedBrowsers = browserslist(undefined, { path: getSystemPath(projectRoot), }); // tslint:disable-next-line:no-implicit-dependencies const { feature, features } = await import('caniuse-lite'); const data = feature(features['es6-module']); return supportedBrowsers .some(browser => { const [agentId, version] = browser.split(' '); const browserData = data.stats[agentId]; const featureStatus = (browserData && browserData[version]) as string | undefined; // We are only interested in the first character // Ex: when 'a #4 #5', we only need to check for 'a' // as for such cases we should polyfill these features as needed return !featureStatus || !criteria.includes(featureStatus.charAt(0)); }); } catch { return undefined; } }