feat(webpack): remove support for legacy browsers (#14190)

This commit is contained in:
Jack Hsu 2023-01-09 05:15:37 -05:00 committed by GitHub
parent 716ba89b15
commit fcc02d1932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 176 additions and 593 deletions

View File

@ -37,7 +37,7 @@
"default": false
},
"skipHelperLibs": {
"description": "Do not install helpers libs (tslib, core-js, regenerator-runtime).",
"description": "Do not install tslib.",
"type": "boolean",
"default": false,
"hidden": true

View File

@ -300,10 +300,6 @@
"description": "Extract CSS into a `.css` file.",
"default": true
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support `ES2015`.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",

View File

@ -307,10 +307,6 @@
"description": "Extract CSS into a `.css` file.",
"default": true
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support `ES2015`.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",

View File

@ -597,7 +597,6 @@ describe('Nx Affected and Graph Tests', () => {
expect(() => checkFilesExist('project-graph.html')).not.toThrow();
expect(() => checkFilesExist('static/styles.css')).not.toThrow();
expect(() => checkFilesExist('static/runtime.js')).not.toThrow();
expect(() => checkFilesExist('static/polyfills.js')).not.toThrow();
expect(() => checkFilesExist('static/main.js')).not.toThrow();
expect(() => checkFilesExist('static/environment.js')).not.toThrow();

View File

@ -26,10 +26,7 @@ describe('file-server', () => {
const p = await runCommandUntil(
`serve ${appName} --port=${port}`,
(output) => {
return (
output.indexOf('webpack compiled') > -1 &&
output.indexOf(`localhost:${port}`) > -1
);
return output.indexOf(`localhost:${port}`) > -1;
}
);
@ -39,5 +36,5 @@ describe('file-server', () => {
} catch {
// ignore
}
}, 1000000);
}, 300_000);
});

View File

@ -112,23 +112,6 @@ describe('Web Components Applications', () => {
checkFilesExist(`dist/libs/${libName}/_should_keep.txt`);
}, 120000);
it('should do another build if differential loading is needed', async () => {
const appName = uniq('app');
runCLI(
`generate @nrwl/web:app ${appName} --bundler=webpack --no-interactive`
);
updateFile(`apps/${appName}/browserslist`, `IE 9-11`);
runCLI(`build ${appName} --outputHashing=none`);
checkFilesExist(
`dist/apps/${appName}/main.js`,
`dist/apps/${appName}/main.es5.js`
);
}, 120000);
it('should emit decorator metadata when it is enabled in tsconfig', async () => {
const appName = uniq('app');
runCLI(

View File

@ -222,7 +222,6 @@ function buildTargetWebpack(
const defaultWebpack = getWebpackConfig(
context,
options,
true,
isScriptOptimizeOn,
{
root: ctProjectConfig.root,

View File

@ -1,11 +1,7 @@
import {
ExecutorContext,
joinPathFragments,
logger,
ProjectGraph,
readJsonFile,
readNxJson,
TargetConfiguration,
workspaceRoot,
} from '@nrwl/devkit';
import { getBaseWebpackPartial } from '@nrwl/webpack/src/utils/config';
@ -17,7 +13,6 @@ import { gte } from 'semver';
import { Configuration, DefinePlugin, WebpackPluginInstance } from 'webpack';
import * as mergeWebpack from 'webpack-merge';
import { mergePlugins } from './merge-plugins';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
const reactWebpackConfig = require('../webpack');
@ -119,14 +114,12 @@ export const webpack = async (
target: 'web',
};
const esm = true;
const isScriptOptimizeOn = storybookWebpackConfig.mode !== 'development';
const extractCss = storybookWebpackConfig.mode === 'production';
// ESM build for modern browsers.
const baseWebpackConfig = mergeWebpack.merge([
getBaseWebpackPartial(builderOptions, {
esm,
isScriptOptimizeOn,
skipTypeCheck: true,
}),

View File

@ -73,7 +73,6 @@ describe('app', () => {
await applicationGenerator(appTree, schema);
expect(appTree.exists('apps/my-app/.babelrc')).toBeTruthy();
expect(appTree.exists('apps/my-app/.browserslistrc')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/main.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy();
@ -312,7 +311,6 @@ describe('app', () => {
main: 'apps/my-app/src/main.tsx',
baseHref: '/',
outputPath: 'dist/apps/my-app',
polyfills: 'apps/my-app/src/polyfills.ts',
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',
@ -810,19 +808,6 @@ describe('app', () => {
).toBeUndefined();
});
it('should add required polyfills for core-js and regenerator', async () => {
await applicationGenerator(appTree, {
...schema,
});
const polyfillsSource = appTree
.read('apps/my-app/src/polyfills.ts')
.toString();
expect(polyfillsSource).toContain('regenerator');
expect(polyfillsSource).toContain('core-js');
});
describe('--skipWorkspaceJson', () => {
it('should update workspace with defaults when --skipWorkspaceJson=false', async () => {
await applicationGenerator(appTree, {

View File

@ -1,16 +0,0 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@ -1,7 +0,0 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';

View File

@ -57,10 +57,6 @@ function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
options.appProjectRoot,
maybeJs(options, `src/main.tsx`)
),
polyfills: joinPathFragments(
options.appProjectRoot,
maybeJs(options, 'src/polyfills.ts')
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [
joinPathFragments(options.appProjectRoot, 'src/favicon.ico'),

View File

@ -52,8 +52,6 @@ function updateDependencies(host: Tree, schema: InitSchema) {
};
if (!schema.skipHelperLibs) {
dependencies['core-js'] = '^3.6.5';
dependencies['regenerator-runtime'] = '0.13.7';
dependencies['tslib'] = tsLibVersion;
}

View File

@ -34,7 +34,7 @@
"default": false
},
"skipHelperLibs": {
"description": "Do not install helpers libs (tslib, core-js, regenerator-runtime).",
"description": "Do not install tslib.",
"type": "boolean",
"default": false,
"hidden": true

View File

@ -43,7 +43,6 @@ module.exports = function (api: any, options: NxWebBabelPresetOptions = {}) {
? { targets: { node: 'current' }, loose: true }
: {
// Allow importing core-js in entrypoint and use browserlist to select polyfills.
// This is needed for differential loading as well.
useBuiltIns: options.useBuiltIns ?? 'entry',
corejs: 3,
// Do not transform modules to CJS

View File

@ -176,10 +176,6 @@
"description": "Extract CSS into a `.css` file.",
"default": true
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support `ES2015`.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",

View File

@ -348,7 +348,6 @@ describe('app', () => {
baseHref: '/',
main: 'apps/my-app/src/main.ts',
outputPath: 'dist/apps/my-app',
polyfills: 'apps/my-app/src/polyfills.ts',
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',

View File

@ -91,10 +91,6 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
'src/index.html'
);
buildOptions.baseHref = '/';
buildOptions.polyfills = joinPathFragments(
options.appProjectRoot,
'src/polyfills.ts'
);
buildOptions.styles = [
joinPathFragments(options.appProjectRoot, `src/styles.${options.style}`),
];

View File

@ -1,7 +0,0 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';

View File

@ -7,5 +7,11 @@
"factory": "./src/migrations/update-15-0-0/add-babel-inputs"
}
},
"remove-es2015-polyfills-option": {
"cli": "nx",
"version": "15.4.5-beta.0",
"description": "Removes es2015Polyfills option since legacy browsers are no longer supported.",
"factory": "./src/migrations/update-15-4-5/remove-es2015-polyfills-option"
},
"packageJsonUpdates": {}
}

View File

@ -34,8 +34,6 @@
"@nrwl/workspace": "file:../workspace",
"autoprefixer": "^10.4.9",
"babel-loader": "^8.2.2",
"browserslist": "^4.21.4",
"caniuse-lite": "^1.0.30001394",
"chalk": "4.1.0",
"chokidar": "^3.5.1",
"copy-webpack-plugin": "^10.2.4",

View File

@ -23,7 +23,6 @@ export function getDevServerConfig(
const webpackConfig = getWebpackConfig(
context,
buildOptions,
true,
typeof buildOptions.optimization === 'boolean'
? buildOptions.optimization
: buildOptions.optimization?.scripts
@ -53,7 +52,6 @@ export function getDevServerConfig(
deployUrl,
sri: subresourceIntegrity,
moduleEntrypoints: [],
noModuleEntrypoints: ['polyfills-es5'],
})
);

View File

@ -1,7 +1,6 @@
import * as path from 'path';
import { posix, resolve } from 'path';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { ScriptTarget } from 'typescript';
import { getHashDigest, interpolateName } from 'loader-utils';
import type { Configuration } from 'webpack';
@ -34,7 +33,6 @@ interface GetWebpackConfigOverrides {
export function getWebpackConfig(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
esm?: boolean,
isScriptOptimizeOn?: boolean,
overrides?: GetWebpackConfigOverrides
): Configuration {
@ -53,19 +51,11 @@ export function getWebpackConfig(
sourceRoot = project.sourceRoot;
}
if (isScriptOptimizeOn) {
// Angular CLI uses an environment variable (NG_BUILD_DIFFERENTIAL_FULL)
// to determine whether to use the scriptTargetOverride
// or the tsConfig target
// We want to force the target if overridden
tsConfig.options.target = ScriptTarget.ES5;
}
const wco: any = {
root: workspaceRoot,
projectRoot: resolve(workspaceRoot, projectRoot),
sourceRoot: resolve(workspaceRoot, sourceRoot),
buildOptions: convertBuildOptions(options),
esm,
console,
tsConfig,
tsConfigPath: options.tsConfig,
@ -75,18 +65,12 @@ export function getWebpackConfig(
_getBaseWebpackPartial(
context,
options,
esm,
isScriptOptimizeOn,
tsConfig.options.emitDecoratorMetadata,
overrides
),
options.target === 'web'
? getPolyfillsPartial(
options.polyfills,
options.es2015Polyfills,
esm,
isScriptOptimizeOn
)
? getPolyfillsPartial(options.polyfills, isScriptOptimizeOn)
: {},
options.target === 'web'
? getStylesPartial(
@ -105,7 +89,6 @@ export function getWebpackConfig(
function _getBaseWebpackPartial(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
esm: boolean,
isScriptOptimizeOn: boolean,
emitDecoratorMetadata: boolean,
overrides?: GetWebpackConfigOverrides
@ -113,7 +96,6 @@ function _getBaseWebpackPartial(
let partial = getBaseWebpackPartial(
options,
{
esm,
isScriptOptimizeOn,
emitDecoratorMetadata,
configuration: overrides?.configuration ?? context.configurationName,
@ -271,33 +253,23 @@ export function getStylesPartial(
export function getPolyfillsPartial(
polyfills: string,
es2015Polyfills: string,
esm: boolean,
isScriptOptimizeOn: boolean
): Configuration {
const config = {
entry: {} as { [key: string]: string[] },
};
if (polyfills && esm && isScriptOptimizeOn) {
if (polyfills && isScriptOptimizeOn) {
// Safari 10.1 supports <script type="module"> but not <script nomodule>.
// Need to patch it up so the browser doesn't load both sets.
config.entry.polyfills = [
require.resolve('@nrwl/webpack/src/utils/webpack/safari-nomodule.js'),
...(polyfills ? [polyfills] : []),
];
} else if (es2015Polyfills && !esm && isScriptOptimizeOn) {
config.entry.polyfills = [
es2015Polyfills,
...(polyfills ? [polyfills] : []),
];
} else {
if (polyfills) {
config.entry.polyfills = [polyfills];
}
if (es2015Polyfills) {
config.entry['polyfills-es5'] = [es2015Polyfills];
}
}
return config;

View File

@ -5,8 +5,8 @@ import { normalizePath } from '@nrwl/devkit';
import type {
AssetGlobPattern,
FileReplacement,
WebpackExecutorOptions,
NormalizedWebpackExecutorOptions,
WebpackExecutorOptions,
} from '../schema';
export function normalizeOptions(
@ -34,11 +34,9 @@ export function normalizeOptions(
}
: options.optimization,
polyfills: options.polyfills ? resolve(root, options.polyfills) : undefined,
es2015Polyfills: options.es2015Polyfills
? resolve(root, options.es2015Polyfills)
: undefined,
};
}
function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]

View File

@ -47,7 +47,6 @@ export interface WebpackExecutorOptions {
crossOrigin?: CrossOriginValue;
deleteOutputPath?: boolean;
deployUrl?: string;
es2015Polyfills?: string;
externalDependencies?: 'all' | 'none' | string[];
extractCss?: boolean;
extractLicenses?: boolean;

View File

@ -228,10 +228,6 @@
"description": "Extract CSS into a `.css` file.",
"default": true
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support `ES2015`.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",

View File

@ -1,26 +1,18 @@
import 'dotenv/config';
import { ExecutorContext, logger } from '@nrwl/devkit';
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await';
import type { Configuration, Stats } from 'webpack';
import { from, of } from 'rxjs';
import {
bufferCount,
mergeMap,
mergeScan,
switchMap,
tap,
} from 'rxjs/operators';
import type { Configuration } from 'webpack';
import { of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { basename, join, resolve } from 'path';
import {
calculateProjectDependencies,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { getWebpackConfig } from './lib/get-webpack-config';
import { getEmittedFiles } from './lib/get-emitted-files';
import { runWebpack } from './lib/run-webpack';
import { BuildBrowserFeatures } from '../../utils/webpack/build-browser-features';
import { deleteOutputDir } from '../../utils/fs';
import { writeIndexHtml } from '../../utils/webpack/write-index-html';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
@ -34,7 +26,7 @@ import { EmittedFile } from '../../utils/models';
async function getWebpackConfigs(
options: NormalizedWebpackExecutorOptions,
context: ExecutorContext
): Promise<Configuration[]> {
): Promise<Configuration> {
const metadata = context.projectsConfigurations.projects[context.projectName];
const projectRoot = metadata.root;
const isScriptOptimizeOn =
@ -43,13 +35,6 @@ async function getWebpackConfigs(
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
const tsConfig = readTsConfig(options.tsConfig);
const scriptTarget = tsConfig.options.target;
const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot,
scriptTarget
);
let customWebpack = null;
@ -64,19 +49,8 @@ async function getWebpackConfigs(
}
}
return await Promise.all(
[
// ESM build for modern browsers.
getWebpackConfig(context, options, true, isScriptOptimizeOn),
// ES5 build for legacy browsers.
options.target === 'web' &&
isScriptOptimizeOn &&
buildBrowserFeatures.isDifferentialLoadingNeeded()
? getWebpackConfig(context, options, false, isScriptOptimizeOn)
: undefined,
]
.filter(Boolean)
.map(async (config) => {
const config = getWebpackConfig(context, options, isScriptOptimizeOn);
if (customWebpack) {
return await customWebpack(config, {
options,
@ -86,8 +60,6 @@ async function getWebpackConfigs(
} else {
return config;
}
})
);
}
export type WebpackExecutorEvent =
@ -162,40 +134,26 @@ export async function* webpackExecutor(
const configs = await getWebpackConfigs(options, context);
return yield* eachValueFrom(
from(configs).pipe(
mergeMap((config) => (Array.isArray(config) ? from(config) : of(config))),
// Run build sequentially and bail when first one fails.
mergeScan(
(acc, config) => {
if (!acc.hasErrors()) {
of(configs).pipe(
switchMap((config) => {
return runWebpack(config).pipe(
tap((stats) => {
console.info(stats.toString(config.stats));
console.info(stats.toString());
})
);
} else {
return of();
}
},
{ hasErrors: () => false } as Stats,
1
),
// Collect build results as an array.
bufferCount(configs.length),
switchMap(async ([result1, result2]) => {
const success =
result1 && !result1.hasErrors() && (!result2 || !result2.hasErrors());
const emittedFiles1 = getEmittedFiles(result1);
const emittedFiles2 = result2 ? getEmittedFiles(result2) : [];
}),
switchMap(async (result) => {
const success = result && !result.hasErrors();
const emittedFiles = getEmittedFiles(result);
if (options.index && options.generateIndexHtml) {
await writeIndexHtml({
crossOrigin: options.crossOrigin,
sri: options.subresourceIntegrity,
outputPath: join(options.outputPath, basename(options.index)),
indexPath: join(context.root, options.index),
files: emittedFiles1.filter((x) => x.extension === '.css'),
noModuleFiles: emittedFiles2,
moduleFiles: emittedFiles1,
files: emittedFiles.filter((x) => x.extension === '.css'),
noModuleFiles: [],
moduleFiles: emittedFiles,
baseHref: options.baseHref,
deployUrl: options.deployUrl,
scripts: options.scripts,
@ -209,7 +167,7 @@ export async function* webpackExecutor(
options.outputPath,
options.outputFileName
),
emittedFiles: [...emittedFiles1, ...emittedFiles2],
emittedFiles,
options,
};
})

View File

@ -0,0 +1,67 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
addProjectConfiguration,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import update from './remove-es2015-polyfills-option';
describe('15.4.5 migration (remove es2015-polyfills)', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should update all executors using @nrwl/webpack:webpack and es2015Polyfills option', async () => {
addProjectConfiguration(tree, 'app1', {
root: 'app1',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
options: {
es2015Polyfills: 'app1/polyfills.ts',
},
},
},
});
addProjectConfiguration(tree, 'app2', {
root: 'app2',
targets: {
custom: {
executor: '@nrwl/webpack:webpack',
options: {
es2016Polyfills: 'app2/polyfills.ts',
},
},
},
});
addProjectConfiguration(tree, 'app3', {
root: 'app3',
targets: {
custom: {
executor: '@foo/bar:faz',
options: {
es2015Polyfills: 'app3/polyfills.ts',
},
},
},
});
await update(tree);
expect(
readProjectConfiguration(tree, 'app1').targets.build.options
.es2015Polyfills
).toBeUndefined();
expect(
readProjectConfiguration(tree, 'app2').targets.custom.options
.es2015Polyfills
).toBeUndefined();
// Another executor, left intact.
expect(
readProjectConfiguration(tree, 'app3').targets.custom.options
.es2015Polyfills
).toEqual('app3/polyfills.ts');
});
});

View File

@ -0,0 +1,22 @@
import { getProjects, Tree, updateProjectConfiguration } from '@nrwl/devkit';
export default async function (tree: Tree) {
const projects = getProjects(tree);
projects.forEach((p) => {
let shouldUpdate = false;
Object.entries(p.targets).forEach(([name, config]) => {
if (
p.targets?.[name]?.executor === '@nrwl/webpack:webpack' &&
p.targets?.[name]?.options.es2015Polyfills
) {
delete p.targets?.[name]?.options.es2015Polyfills;
shouldUpdate = true;
}
});
if (shouldUpdate) {
updateProjectConfiguration(tree, p.name, p);
}
});
}

View File

@ -23,7 +23,6 @@ const IGNORED_WEBPACK_WARNINGS = [
];
export interface InternalBuildOptions {
esm?: boolean;
isScriptOptimizeOn?: boolean;
emitDecoratorMetadata?: boolean;
configuration?: string;
@ -40,18 +39,13 @@ export function getBaseWebpackPartial(
// If the function is called directly and not through `@nrwl/webpack:webpack` then this target may not be set.
options.target ??= 'web';
const mainFields = [
...(internalOptions.esm ? ['es2015'] : []),
'module',
'main',
];
const mainFields = ['es2015', 'module', 'main'];
const hashFormat = getOutputHashFormat(options.outputHashing);
const suffixFormat = internalOptions.esm ? '' : '.es5';
const filename = internalOptions.isScriptOptimizeOn
? `[name]${hashFormat.script}${suffixFormat}.js`
? `[name]${hashFormat.script}.js`
: '[name].js';
const chunkFilename = internalOptions.isScriptOptimizeOn
? `[name]${hashFormat.chunk}${suffixFormat}.js`
? `[name]${hashFormat.chunk}.js`
: '[name].js';
const mode = internalOptions.isScriptOptimizeOn
? 'production'
@ -90,7 +84,7 @@ export function getBaseWebpackPartial(
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
scriptType: internalOptions.esm ? 'module' : undefined,
scriptType: 'module',
},
module: {
// Enabled for performance
@ -180,14 +174,21 @@ export function getBaseWebpackPartial(
webpackConfig.plugins.push(
new webpack.DefinePlugin(getClientEnvironment(mode).stringified)
);
if (options.compiler !== 'swc' && internalOptions.isScriptOptimizeOn) {
webpackConfig.optimization ??= {};
webpackConfig.optimization.nodeEnv = process.env.NODE_ENV ?? mode;
if (internalOptions.isScriptOptimizeOn) {
// Always check sideEffects field in package.json for tree-shaking to work.
webpackConfig.optimization.sideEffects = true;
if (options.compiler !== 'swc') {
webpackConfig.optimization = {
sideEffects: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: (internalOptions.esm ? 2016 : 5) as TerserPlugin.TerserECMA,
ecma: 2020,
safari10: true,
output: {
ascii_only: true,
@ -200,13 +201,12 @@ export function getBaseWebpackPartial(
runtimeChunk: true,
};
}
webpackConfig.optimization ??= {};
webpackConfig.optimization.nodeEnv = process.env.NODE_ENV ?? mode;
}
}
const extraPlugins: WebpackPluginInstance[] = [];
if (!internalOptions.skipTypeCheck && internalOptions.esm) {
if (!internalOptions.skipTypeCheck) {
extraPlugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
@ -412,7 +412,7 @@ export function createLoaderFromCompiler(
rootMode: 'upward',
cwd: join(options.root, options.sourceRoot),
emitDecoratorMetadata: extraOptions.emitDecoratorMetadata,
isModern: extraOptions.esm,
isModern: true,
envName: extraOptions.isScriptOptimizeOn
? 'production'
: extraOptions.configuration,

View File

@ -25,5 +25,4 @@ export interface CreateWebpackConfigOptions<T = any> {
buildOptions: T;
tsConfig: any;
tsConfigPath: string;
supportES2015: boolean;
}

View File

@ -1,161 +0,0 @@
import { fs, vol } from 'memfs';
jest.mock('fs', () => fs);
import { ScriptTarget } from 'typescript';
// Disable browserslist cache so that each test resolves a new config.
process.env.BROWSERSLIST_DISABLE_CACHE = 'true';
import { BuildBrowserFeatures } from './build-browser-features';
describe('BuildBrowserFeatures', () => {
beforeEach(async () => {
vol.fromJSON(
{
'.browserslistrc': '',
},
'/root'
);
});
describe('isDifferentialLoadingNeeded', () => {
it('should be true for for IE 9-11 and ES2015', () => {
fs.writeFileSync('/root/.browserslistrc', 'IE 9-11');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(true);
});
it('should be false for Chrome and ES2015', () => {
fs.writeFileSync('/root/.browserslistrc', 'last 1 chrome version');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
});
it('detects no need for differential loading for target is ES5', () => {
fs.writeFileSync('/root/.browserslistrc', 'last 1 chrome version');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES5
);
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
});
it('should be false for Safari 10.1 when target is ES2015', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari 10.1');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isDifferentialLoadingNeeded()).toBe(false);
});
});
describe('isFeatureSupported', () => {
it('should be true for es6-module and Safari 10.1', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari 10.1');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
});
it('should be false for es6-module and IE9', () => {
fs.writeFileSync('/root/.browserslistrc', 'IE 9');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(false);
});
it('should be true for es6-module and last 1 chrome version', () => {
fs.writeFileSync('/root/.browserslistrc', 'last 1 chrome version');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
});
it('should be true for es6-module and Edge 18', () => {
fs.writeFileSync('/root/.browserslistrc', 'Edge 18');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isFeatureSupported('es6-module')).toBe(true);
});
});
describe('isNoModulePolyfillNeeded', () => {
it('should be false for Safari 10.1 when target is ES5', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari 10.1');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES5
);
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
});
it('should be false for Safari 10.1 when target is ES2015', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari 10.1');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
});
it('should be true for Safari 9+ when target is ES2015', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari >= 9');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(true);
});
it('should be false for Safari 9+ when target is ES5', () => {
fs.writeFileSync('/root/.browserslistrc', 'Safari >= 9');
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES5
);
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
});
it('should be false when not supporting Safari 10.1 target is ES2015', () => {
fs.writeFileSync(
'/root/.browserslistrc',
`
Edge 18
IE 9
`
);
const buildBrowserFeatures = new BuildBrowserFeatures(
'/root',
ScriptTarget.ES2015
);
expect(buildBrowserFeatures.isNoModulePolyfillNeeded()).toBe(false);
});
});
});

View File

@ -1,78 +0,0 @@
import * as browserslist from 'browserslist';
import { feature, features } from 'caniuse-lite';
import * as ts from 'typescript';
export class BuildBrowserFeatures {
private readonly _supportedBrowsers: string[];
private readonly _es6TargetOrLater: boolean;
constructor(
private projectRoot: string,
private scriptTarget: ts.ScriptTarget
) {
this._supportedBrowsers = browserslist(undefined, {
path: this.projectRoot,
});
this._es6TargetOrLater = this.scriptTarget > ts.ScriptTarget.ES5;
}
/**
* True, when one or more browsers requires ES5
* support and the script target is ES2015 or greater.
*/
isDifferentialLoadingNeeded(): boolean {
return this._es6TargetOrLater && this.isEs5SupportNeeded();
}
/**
* True, when one or more browsers requires ES5 support
*/
isEs5SupportNeeded(): boolean {
return !this.isFeatureSupported('es6-module');
}
/**
* Safari 10.1 and iOS Safari 10.3 supports modules,
* but does not support the `nomodule` attribute.
* While return `true`, when support for Safari 10.1 and iOS Safari 10.3
* is required and in differential loading is enabled.
*/
isNoModulePolyfillNeeded(): boolean {
if (!this.isDifferentialLoadingNeeded()) {
return false;
}
const safariBrowsers = ['safari 10.1', 'ios_saf 10.3'];
return this._supportedBrowsers.some((browser) =>
safariBrowsers.includes(browser)
);
}
/**
* True, when a browser feature is supported partially or fully.
*/
isFeatureSupported(featureId: string): boolean {
// y: feature is fully available
// n: feature is unavailable
// a: feature is partially supported
// x: feature is prefixed
const criteria = ['y', 'a'];
const data = feature(features[featureId]);
return !this._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));
});
}
}

View File

@ -22,9 +22,7 @@ export function generateEntryPoints(appConfig: {
};
const entryPoints = [
'polyfills-nomodule-es5',
'runtime',
'polyfills-es5',
'polyfills',
'sw-register',
...extraEntryPoints(appConfig.styles, 'styles'),

View File

@ -5,7 +5,7 @@ import { CreateWebpackConfigOptions } from '../../models';
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
export function getBrowserConfig(wco: CreateWebpackConfigOptions) {
const { buildOptions, supportES2015 } = wco;
const { buildOptions } = wco;
const extraPlugins = [];
const stylesOptimization =
@ -41,12 +41,7 @@ export function getBrowserConfig(wco: CreateWebpackConfigOptions) {
return {
resolve: {
mainFields: [
...(supportES2015 ? ['es2015'] : []),
'browser',
'module',
'main',
],
mainFields: ['browser', 'module', 'main'],
},
output: {
crossOriginLoading: buildOptions.subresourceIntegrity

View File

@ -1,19 +1,17 @@
import { basename, join, resolve } from 'path';
import { basename, resolve } from 'path';
import type { Compiler, Configuration } from 'webpack';
import { ids, ProgressPlugin, sources } from 'webpack';
import { ScriptTarget } from 'typescript';
import { ProgressPlugin, sources } from 'webpack';
import { normalizeExtraEntryPoints } from '../normalize-entry';
import { ScriptsWebpackPlugin } from '../plugins/scripts-webpack-plugin';
import { BuildBrowserFeatures } from '../build-browser-features';
import { getOutputHashFormat } from '../../hash-format';
import { findAllNodeModules, findUp } from '../../fs';
import type { CreateWebpackConfigOptions, ExtraEntryPoint } from '../../models';
import type { CreateWebpackConfigOptions } from '../../models';
export function getCommonConfig(
wco: CreateWebpackConfigOptions
): Configuration {
const { root, projectRoot, sourceRoot, buildOptions, tsConfig } = wco;
const { root, projectRoot, sourceRoot, buildOptions } = wco;
let stylesOptimization: boolean;
let scriptsOptimization: boolean;
@ -37,48 +35,6 @@ export function getCommonConfig(
entryPoints['main'] = [resolve(root, buildOptions.main)];
}
const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot,
tsConfig.options.target || ScriptTarget.ES5
);
const differentialLoadingNeeded =
buildBrowserFeatures.isDifferentialLoadingNeeded();
if (tsConfig.options.target === ScriptTarget.ES5) {
if (buildBrowserFeatures.isEs5SupportNeeded()) {
// The nomodule polyfill needs to be inject prior to any script and be
// outside of webpack compilation because otherwise webpack will cause the
// script to be wrapped in window["webpackJsonp"] which causes this to fail.
if (buildBrowserFeatures.isNoModulePolyfillNeeded()) {
const noModuleScript: ExtraEntryPoint = {
bundleName: 'polyfills-nomodule-es5',
input: join(__dirname, '..', 'safari-nomodule.js'),
};
buildOptions.scripts = buildOptions.scripts
? [...buildOptions.scripts, noModuleScript]
: [noModuleScript];
}
// For full build differential loading we don't need to generate a seperate polyfill file
// because they will be loaded exclusivly based on module and nomodule
const polyfillsChunkName = differentialLoadingNeeded
? 'polyfills'
: 'polyfills-es5';
entryPoints[polyfillsChunkName] = [
join(__dirname, '..', 'es5-polyfills.js'),
];
// If not performing a full differential build the polyfills need to be added to ES5 bundle
if (buildOptions.polyfills) {
entryPoints[polyfillsChunkName].push(
resolve(root, buildOptions.polyfills)
);
}
}
}
if (buildOptions.polyfills) {
entryPoints['polyfills'] = [
...(entryPoints['polyfills'] || []),
@ -179,26 +135,12 @@ export function getCommonConfig(
const loaderNodeModules = findAllNodeModules(__dirname, projectRoot);
loaderNodeModules.unshift('node_modules');
// Load rxjs path aliases.
// https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md#build-and-treeshaking
let alias = {};
try {
const rxjsPathMappingImport = wco.supportES2015
? 'rxjs/_esm2015/path-mapping'
: 'rxjs/_esm5/path-mapping';
const rxPaths = require(require.resolve(rxjsPathMappingImport, {
paths: [projectRoot],
}));
alias = rxPaths(nodeModules);
} catch {}
return {
profile: buildOptions.statsJson,
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js'],
symlinks: true,
modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'],
alias,
},
resolveLoader: {
modules: loaderNodeModules,

View File

@ -21,7 +21,6 @@ export interface IndexHtmlGeneratorProcessOptions {
baseHref?: string | undefined;
outputPath: string;
files: FileInfo[];
noModuleFiles: FileInfo[];
moduleFiles: FileInfo[];
}
@ -111,14 +110,7 @@ function augmentIndexHtmlPlugin(
} = generator.options;
return async (html, options) => {
const {
lang,
baseHref,
outputPath = '',
noModuleFiles,
files,
moduleFiles,
} = options;
const { lang, baseHref, outputPath = '', files, moduleFiles } = options;
return augmentIndexHtml({
html,
@ -130,7 +122,6 @@ function augmentIndexHtmlPlugin(
entrypoints,
loadOutputFile: (filePath) =>
generator.readAsset(join(outputPath, filePath)),
noModuleFiles,
moduleFiles,
files,
});

View File

@ -13,7 +13,6 @@ export interface IndexHtmlWebpackPluginOptions
IndexHtmlGeneratorProcessOptions,
'files' | 'noModuleFiles' | 'moduleFiles'
> {
noModuleEntrypoints: string[];
moduleEntrypoints: string[];
}
@ -59,7 +58,6 @@ export class IndexHtmlWebpackPlugin extends IndexHtmlGenerator {
const callback = async (assets: Record<string, unknown>) => {
// Get all files for selected entrypoints
const files: FileInfo[] = [];
const noModuleFiles: FileInfo[] = [];
const moduleFiles: FileInfo[] = [];
try {
@ -79,9 +77,7 @@ export class IndexHtmlWebpackPlugin extends IndexHtmlGenerator {
continue;
}
if (this.options.noModuleEntrypoints.includes(entryName)) {
noModuleFiles.push(...entryFiles);
} else if (this.options.moduleEntrypoints.includes(entryName)) {
if (this.options.moduleEntrypoints.includes(entryName)) {
moduleFiles.push(...entryFiles);
} else {
files.push(...entryFiles);
@ -90,7 +86,6 @@ export class IndexHtmlWebpackPlugin extends IndexHtmlGenerator {
const { content, warnings, errors } = await this.process({
files,
noModuleFiles,
moduleFiles,
outputPath: dirname(this.options.outputPath),
baseHref: this.options.baseHref,

View File

@ -59,13 +59,7 @@ export interface FileInfo {
* bundles for differential serving.
*/
export function augmentIndexHtml(params: AugmentIndexHtmlOptions): string {
const {
loadOutputFile,
files,
noModuleFiles = [],
moduleFiles = [],
entrypoints,
} = params;
const { loadOutputFile, files, moduleFiles = [], entrypoints } = params;
let { crossOrigin = 'none' } = params;
if (params.sri && crossOrigin === 'none') {
@ -76,7 +70,7 @@ export function augmentIndexHtml(params: AugmentIndexHtmlOptions): string {
const scripts = new Set<string>();
// Sort files in the order we want to insert them by entrypoint and dedupes duplicates
const mergedFiles = [...moduleFiles, ...noModuleFiles, ...files];
const mergedFiles = [...moduleFiles, ...files];
for (const entrypoint of entrypoints) {
for (const { extension, file, name } of mergedFiles) {
if (name !== entrypoint) {
@ -160,15 +154,9 @@ export function augmentIndexHtml(params: AugmentIndexHtmlOptions): string {
// in some cases for differential loading file with the same name is avialable in both
// nomodule and module such as scripts.js
// we shall not add these attributes if that's the case
const isNoModuleType = noModuleFiles.some(scriptPredictor);
const isModuleType = moduleFiles.some(scriptPredictor);
if (isNoModuleType && !isModuleType) {
attrs.push({ name: 'nomodule', value: null });
if (!script.startsWith('polyfills-nomodule-es5')) {
attrs.push({ name: 'defer', value: null });
}
} else if (isModuleType) {
if (isModuleType) {
attrs.push({ name: 'type', value: 'module' });
} else {
attrs.push({ name: 'defer', value: null });
@ -293,7 +281,6 @@ export async function writeIndexHtml({
outputPath,
indexPath,
files = [],
noModuleFiles = [],
moduleFiles = [],
baseHref,
deployUrl,
@ -314,7 +301,6 @@ export async function writeIndexHtml({
sri,
entrypoints: generateEntryPoints({ scripts, styles }),
files: filterAndMapBuildFiles(files, ['.js', '.css']),
noModuleFiles: filterAndMapBuildFiles(noModuleFiles, '.js'),
moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'),
loadOutputFile: (filePath) =>
readFileSync(join(dirname(outputPath), filePath)).toString(),