From 811c50b92c21e27a6ac84afc8ea551870b4be989 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Fri, 30 Aug 2019 11:04:59 -0400 Subject: [PATCH] feat(web): use babel-loader instead of ts-loader for web build builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - removes the `differentialLoading` build option - differential loading is always enabled for prod builds BEFORE (without ESM): Benchmark #1: nx build demo --prod Time (mean ± σ): 13.834 s ± 1.731 s [User: 11.817 s, System: 1.352 s] Range (min … max): 11.947 s … 16.015 s 10 runs AFTER (with ESM): Benchmark #1: nx build demo --prod Time (mean ± σ): 18.711 s ± 1.310 s [User: 12.172 s, System: 1.394 s] Range (min … max): 17.232 s … 20.770 s 10 runs --- .../api-react/schematics/application.md | 8 - docs/angular/api-web/builders/build.md | 8 - .../react/api-react/schematics/application.md | 8 - docs/react/api-web/builders/build.md | 8 - docs/web/api-react/schematics/application.md | 8 - docs/web/api-web/builders/build.md | 8 - e2e/delegate-to-cli.test.ts | 16 +- e2e/react.test.ts | 16 +- e2e/utils.ts | 1 + e2e/web.test.ts | 28 +-- package.json | 3 +- packages/node/package.json | 8 +- packages/react/migrations.json | 5 + packages/react/plugins/babel.ts | 24 ++- .../update-workspace-8-5-0.spec.ts | 57 +++++ .../update-8-5-0/update-workspace-8-5-0.ts | 24 +++ packages/react/src/plugins/babel.ts | 43 ---- .../application/application.spec.ts | 53 +++-- .../src/schematics/application/application.ts | 42 +--- .../src/schematics/application/schema.d.ts | 1 - .../src/schematics/application/schema.json | 5 - packages/react/src/utils/versions.ts | 8 - packages/web/migrations.json | 8 +- packages/web/package.json | 19 +- .../web/src/builders/build/build.impl.spec.ts | 5 +- packages/web/src/builders/build/build.impl.ts | 201 ++++++++++-------- packages/web/src/builders/build/schema.json | 5 - .../builders/dev-server/dev-server.impl.ts | 4 +- .../update-8-5-0/update-builder-8-5-0.spec.ts | 54 +++++ .../update-8-5-0/update-builder-8-5-0.ts | 21 ++ packages/web/src/utils/config.spec.ts | 63 ++---- packages/web/src/utils/config.ts | 116 ++++++---- .../web/src/utils/devserver.config.spec.ts | 1 - packages/web/src/utils/devserver.config.ts | 4 +- packages/web/src/utils/hash-format.ts | 28 +++ packages/web/src/utils/normalize.ts | 7 +- packages/web/src/utils/types.ts | 1 + packages/web/src/utils/web.config.spec.ts | 28 +-- packages/web/src/utils/web.config.ts | 64 +++--- yarn.lock | 191 +++++++---------- 40 files changed, 635 insertions(+), 567 deletions(-) create mode 100644 packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.spec.ts create mode 100644 packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.ts delete mode 100644 packages/react/src/plugins/babel.ts create mode 100644 packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.spec.ts create mode 100644 packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.ts create mode 100644 packages/web/src/utils/hash-format.ts diff --git a/docs/angular/api-react/schematics/application.md b/docs/angular/api-react/schematics/application.md index 3f09aca06a873..e6be1bce33fb6 100644 --- a/docs/angular/api-react/schematics/application.md +++ b/docs/angular/api-react/schematics/application.md @@ -48,14 +48,6 @@ ng g app myapp --routing ## Options -### babel - -Default: `false` - -Type: `boolean` - -Use Babel and TypeScript preset instead of ts-loader (Useful if you need Babel plugins) - ### classComponent Alias(es): C diff --git a/docs/angular/api-web/builders/build.md b/docs/angular/api-web/builders/build.md index 1121384c4b6fc..7fde617fd2fad 100644 --- a/docs/angular/api-web/builders/build.md +++ b/docs/angular/api-web/builders/build.md @@ -40,14 +40,6 @@ Type: `string` URL where the application will be deployed. -### differentialLoading - -Default: `true` - -Type: `boolean` - -Enable differential loading for es5 browsers - ### es2015Polyfills Type: `string` diff --git a/docs/react/api-react/schematics/application.md b/docs/react/api-react/schematics/application.md index 2938e3e1e6a29..38616ab759d19 100644 --- a/docs/react/api-react/schematics/application.md +++ b/docs/react/api-react/schematics/application.md @@ -48,14 +48,6 @@ nx g app myapp --routing ## Options -### babel - -Default: `false` - -Type: `boolean` - -Use Babel and TypeScript preset instead of ts-loader (Useful if you need Babel plugins) - ### classComponent Alias(es): C diff --git a/docs/react/api-web/builders/build.md b/docs/react/api-web/builders/build.md index 245854d6d338d..b47f8d037ce7b 100644 --- a/docs/react/api-web/builders/build.md +++ b/docs/react/api-web/builders/build.md @@ -41,14 +41,6 @@ Type: `string` URL where the application will be deployed. -### differentialLoading - -Default: `true` - -Type: `boolean` - -Enable differential loading for es5 browsers - ### es2015Polyfills Type: `string` diff --git a/docs/web/api-react/schematics/application.md b/docs/web/api-react/schematics/application.md index 2938e3e1e6a29..38616ab759d19 100644 --- a/docs/web/api-react/schematics/application.md +++ b/docs/web/api-react/schematics/application.md @@ -48,14 +48,6 @@ nx g app myapp --routing ## Options -### babel - -Default: `false` - -Type: `boolean` - -Use Babel and TypeScript preset instead of ts-loader (Useful if you need Babel plugins) - ### classComponent Alias(es): C diff --git a/docs/web/api-web/builders/build.md b/docs/web/api-web/builders/build.md index ab152916cc3fa..9215904520a04 100644 --- a/docs/web/api-web/builders/build.md +++ b/docs/web/api-web/builders/build.md @@ -41,14 +41,6 @@ Type: `string` URL where the application will be deployed. -### differentialLoading - -Default: `true` - -Type: `boolean` - -Enable differential loading for es5 browsers - ### es2015Polyfills Type: `string` diff --git a/e2e/delegate-to-cli.test.ts b/e2e/delegate-to-cli.test.ts index 90a6158d9bf27..4cf5a845c357d 100644 --- a/e2e/delegate-to-cli.test.ts +++ b/e2e/delegate-to-cli.test.ts @@ -13,18 +13,16 @@ forEachCli(() => { const appName = uniq('app'); runCommand(`npm run nx -- g @nrwl/web:app ${appName}`); - runCommand(`npm run nx -- build ${appName}`); + runCommand(`npm run nx -- build ${appName} --prod --outputHashing none`); checkFilesExist( `dist/apps/${appName}/index.html`, - `dist/apps/${appName}/polyfills-es2015.js`, - `dist/apps/${appName}/runtime-es2015.js`, - `dist/apps/${appName}/main-es2015.js`, - `dist/apps/${appName}/styles-es2015.js`, - `dist/apps/${appName}/polyfills-es5.js`, - `dist/apps/${appName}/runtime-es5.js`, - `dist/apps/${appName}/main-es5.js`, - `dist/apps/${appName}/styles-es5.js` + `dist/apps/${appName}/runtime.js`, + `dist/apps/${appName}/polyfills.esm.js`, + `dist/apps/${appName}/main.esm.js`, + `dist/apps/${appName}/polyfills.es5.js`, + `dist/apps/${appName}/main.es5.js`, + `dist/apps/${appName}/styles.css` ); }, 120000); }); diff --git a/e2e/react.test.ts b/e2e/react.test.ts index d77f05c6d2574..6cb299d4b548e 100644 --- a/e2e/react.test.ts +++ b/e2e/react.test.ts @@ -24,7 +24,7 @@ forEachCli(currentCLIName => { const libName = uniq('lib'); runCLI( - `generate @nrwl/react:app ${appName} --no-interactive --babel --linter=${linter}` + `generate @nrwl/react:app ${appName} --no-interactive --linter=${linter}` ); runCLI(`generate @nrwl/react:lib ${libName} --no-interactive`); @@ -42,7 +42,7 @@ forEachCli(currentCLIName => { const appName = uniq('app'); runCLI( - `generate @nrwl/react:app ${appName} --routing --no-interactive --babel --linter=${linter}` + `generate @nrwl/react:app ${appName} --routing --no-interactive --linter=${linter}` ); await testGeneratedApp(appName, { checkStyles: true, checkLinter: true }); @@ -53,7 +53,7 @@ forEachCli(currentCLIName => { const appName = uniq('app'); runCLI( - `generate @nrwl/react:app ${appName} --style styled-components --no-interactive --babel --linter=${linter}` + `generate @nrwl/react:app ${appName} --style styled-components --no-interactive --linter=${linter}` ); await testGeneratedApp(appName, { @@ -68,7 +68,7 @@ forEachCli(currentCLIName => { const libName = uniq('lib'); runCLI( - `generate @nrwl/react:app ${appName} --no-interactive --babel --linter=${linter}` + `generate @nrwl/react:app ${appName} --no-interactive --linter=${linter}` ); runCLI(`generate @nrwl/react:lib ${libName} --no-interactive`); @@ -129,14 +129,16 @@ forEachCli(currentCLIName => { } checkFilesExist(...filesToCheck); expect(readFile(`dist/apps/${appName}/main.js`)).toContain( - 'var App = function App() {' + 'var App = () => {' ); runCLI(`build ${appName} --prod --output-hashing none`); filesToCheck = [ `dist/apps/${appName}/index.html`, - `dist/apps/${appName}/polyfills.js`, `dist/apps/${appName}/runtime.js`, - `dist/apps/${appName}/main.js` + `dist/apps/${appName}/polyfills.esm.js`, + `dist/apps/${appName}/main.esm.js`, + `dist/apps/${appName}/polyfills.es5.js`, + `dist/apps/${appName}/main.es5.js` ]; if (opts.checkStyles) { filesToCheck.push(`dist/apps/${appName}/styles.css`); diff --git a/e2e/utils.ts b/e2e/utils.ts index 78bfccfbd5d5d..aa94f9a26b25c 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -206,6 +206,7 @@ export function copyMissingPackages(): void { '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript', + '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-decorators', 'babel-loader', 'babel-plugin-macros', diff --git a/e2e/web.test.ts b/e2e/web.test.ts index 5a193cb26156a..5835167314849 100644 --- a/e2e/web.test.ts +++ b/e2e/web.test.ts @@ -26,30 +26,22 @@ forEachCli(currentCLIName => { runCLI(`build ${appName}`); checkFilesExist( `dist/apps/${appName}/index.html`, - `dist/apps/${appName}/polyfills-es2015.js`, - `dist/apps/${appName}/runtime-es2015.js`, - `dist/apps/${appName}/main-es2015.js`, - `dist/apps/${appName}/styles-es2015.js`, - `dist/apps/${appName}/polyfills-es5.js`, - `dist/apps/${appName}/runtime-es5.js`, - `dist/apps/${appName}/main-es5.js`, - `dist/apps/${appName}/styles-es5.js` + `dist/apps/${appName}/runtime.js`, + `dist/apps/${appName}/polyfills.js`, + `dist/apps/${appName}/main.js`, + `dist/apps/${appName}/styles.js` ); - expect(readFile(`dist/apps/${appName}/main-es5.js`)).toContain( - 'var AppElement = /** @class */ (function (_super) {' - ); - expect(readFile(`dist/apps/${appName}/main-es2015.js`)).toContain( + expect(readFile(`dist/apps/${appName}/main.js`)).toContain( 'class AppElement' ); runCLI(`build ${appName} --prod --output-hashing none`); checkFilesExist( `dist/apps/${appName}/index.html`, - `dist/apps/${appName}/polyfills-es2015.js`, - `dist/apps/${appName}/runtime-es2015.js`, - `dist/apps/${appName}/main-es2015.js`, - `dist/apps/${appName}/polyfills-es5.js`, - `dist/apps/${appName}/runtime-es5.js`, - `dist/apps/${appName}/main-es5.js`, + `dist/apps/${appName}/runtime.js`, + `dist/apps/${appName}/polyfills.esm.js`, + `dist/apps/${appName}/main.esm.js`, + `dist/apps/${appName}/polyfills.es5.js`, + `dist/apps/${appName}/main.es5.js`, `dist/apps/${appName}/styles.css` ); expect(readFile(`dist/apps/${appName}/index.html`)).toContain( diff --git a/package.json b/package.json index 8234a16572786..bf6378bf65ad1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@angular/router": "^8.0.0", "@angular/upgrade": "^8.0.0", "@babel/core": "7.5.5", + "@babel/plugin-proposal-class-properties": "7.5.5", "@babel/plugin-proposal-decorators": "7.4.4", "@babel/preset-env": "7.5.5", "@babel/preset-react": "7.0.0", @@ -141,7 +142,7 @@ "tslint": "5.11.0", "typescript": "~3.4.5", "viz.js": "^1.8.1", - "webpack": "4.30.0", + "webpack": "4.39.3", "webpack-dev-server": "3.1.14", "webpack-node-externals": "^1.7.2", "yargs": "^11.0.0", diff --git a/packages/node/package.json b/packages/node/package.json index d6c2074cf9816..216b50458ce05 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -39,14 +39,14 @@ "@angular-devkit/schematics": "8.1.1", "@angular-devkit/build-webpack": "0.801.1", "circular-dependency-plugin": "^5.0.2", - "copy-webpack-plugin": "4.6.0", - "fork-ts-checker-webpack-plugin": "0.4.9", + "copy-webpack-plugin": "5.0.3", + "fork-ts-checker-webpack-plugin": "0.4.15", "license-webpack-plugin": "^1.4.0", "source-map-support": "0.5.12", "tree-kill": "1.2.1", - "ts-loader": "5.3.1", + "ts-loader": "5.4.5", "tsconfig-paths-webpack-plugin": "3.2.0", - "webpack": "4.29.0", + "webpack": "4.39.3", "webpack-dev-server": "3.1.14", "webpack-node-externals": "1.7.2" } diff --git a/packages/react/migrations.json b/packages/react/migrations.json index 2e42d93da592c..0b979c7f73851 100644 --- a/packages/react/migrations.json +++ b/packages/react/migrations.json @@ -4,6 +4,11 @@ "version": "8.3.0-beta.1", "description": "Update React libraries", "factory": "./src/migrations/update-8-3-0/update-8-3-0" + }, + "update-workspace-8.5.0": { + "version": "8.5.0-beta.1", + "description": "Update React workspace", + "factory": "./src/migrations/update-8-5-0/update-workspace-8-5-0" } } } diff --git a/packages/react/plugins/babel.ts b/packages/react/plugins/babel.ts index dce1c29a1abc1..5a38effee204e 100644 --- a/packages/react/plugins/babel.ts +++ b/packages/react/plugins/babel.ts @@ -1 +1,23 @@ -module.exports = require('../src/plugins/babel').getBabelWebpackConfig; +import { Configuration } from 'webpack'; + +// Adds react preset for JSX support +function getBabelWebpackConfig(config: Configuration) { + const babelRuleOptions = config.module.rules.find( + r => r.loader === 'babel-loader' + ).options as any; + + const idx = babelRuleOptions.presets.findIndex( + p => Array.isArray(p) && p[0] === '@babel/preset-env' + ); + + babelRuleOptions.presets.splice(idx, 0, [ + '@babel/preset-react', + { + useBuiltIns: true + } + ]); + + return config; +} + +module.exports = getBabelWebpackConfig; diff --git a/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.spec.ts b/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.spec.ts new file mode 100644 index 0000000000000..25020ab97339d --- /dev/null +++ b/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.spec.ts @@ -0,0 +1,57 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { + updateJsonInTree, + readJsonInTree, + updateWorkspaceInTree, + readWorkspace, + getWorkspacePath +} from '@nrwl/workspace'; + +import * as path from 'path'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; + +describe('Update 8-5-0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + schematicRunner = new SchematicTestRunner( + '@nrwl/react', + path.join(__dirname, '../../../migrations.json') + ); + }); + + it(`should remove babel schematic defaults`, async () => { + tree.create( + 'workspace.json', + JSON.stringify({ + schematics: { + '@nrwl/react': { + application: { + babel: true + } + }, + '@nrwl/react:application': { + babel: true + } + } + }) + ); + + tree = await schematicRunner + .runSchematicAsync('update-workspace-8.5.0', {}, tree) + .toPromise(); + + const config = readWorkspace(tree); + expect(config).toEqual({ + schematics: { + '@nrwl/react': { + application: {} + }, + '@nrwl/react:application': {} + } + }); + }); +}); diff --git a/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.ts b/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.ts new file mode 100644 index 0000000000000..b576593b67079 --- /dev/null +++ b/packages/react/src/migrations/update-8-5-0/update-workspace-8-5-0.ts @@ -0,0 +1,24 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updateWorkspaceInTree } from '@nrwl/workspace'; + +export default function update(): Rule { + return updateWorkspaceInTree(config => { + const a = []; + const b = []; + Object.keys(config.schematics).forEach(name => { + if (name === '@nrwl/react' && config.schematics[name].application) { + a.push(config.schematics[name]); + } + if (name === '@nrwl/react:application') { + b.push(config.schematics[name]); + } + }); + a.forEach(x => { + delete x.application.babel; + }); + b.forEach(x => { + delete x.babel; + }); + return config; + }); +} diff --git a/packages/react/src/plugins/babel.ts b/packages/react/src/plugins/babel.ts deleted file mode 100644 index 6eb0ef10d4d7e..0000000000000 --- a/packages/react/src/plugins/babel.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Configuration } from 'webpack'; - -export function getBabelWebpackConfig(config: Configuration) { - const idx = config.module.rules.findIndex(r => r.loader === 'ts-loader'); - - config.module.rules.splice(idx, 1, { - use: { - loader: 'babel-loader', - options: { - presets: [ - [ - require('@babel/preset-env').default, - { - // Include any project files, e.g. browserlist, by setting the context as config path - configPath: config.context, - // Allow importing core-js in entrypoint and use browserlist to select polyfills - useBuiltIns: 'entry', - corejs: 3, - modules: false, - // Exclude transforms that make all code slower - exclude: ['transform-typeof-symbol'] - } - ], - [ - require('@babel/preset-react').default, - { - useBuiltIns: true - } - ], - [require('@babel/preset-typescript').default] - ], - plugins: [ - require('babel-plugin-macros'), - [require('@babel/plugin-proposal-decorators').default, false] - ] - } - }, - test: /\.tsx?|jsx?$/, - exclude: /node_modules/ - }); - - return config; -} diff --git a/packages/react/src/schematics/application/application.spec.ts b/packages/react/src/schematics/application/application.spec.ts index dc7d5f5ec6805..5a92d205e8cd2 100644 --- a/packages/react/src/schematics/application/application.spec.ts +++ b/packages/react/src/schematics/application/application.spec.ts @@ -229,7 +229,6 @@ describe('app', () => { const architectConfig = workspaceJson.projects['my-app'].architect; expect(architectConfig.build.builder).toEqual('@nrwl/web:build'); expect(architectConfig.build.options).toEqual({ - differentialLoading: true, assets: ['apps/my-app/src/favicon.ico', 'apps/my-app/src/assets'], index: 'apps/my-app/src/index.html', main: 'apps/my-app/src/main.tsx', @@ -237,7 +236,8 @@ describe('app', () => { polyfills: 'apps/my-app/src/polyfills.ts', scripts: [], styles: ['apps/my-app/src/styles.css'], - tsConfig: 'apps/my-app/tsconfig.app.json' + tsConfig: 'apps/my-app/tsconfig.app.json', + webpackConfig: '@nrwl/react/plugins/babel' }); expect(architectConfig.build.configurations.production).toEqual({ optimization: true, @@ -467,37 +467,32 @@ describe('app', () => { }); }); - describe('--babel true', () => { - it('should adds custom webpack config', async () => { - const tree = await runSchematic( - 'app', - { name: 'myApp', babel: true }, - appTree - ); + it('should adds custom webpack config', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', babel: true }, + appTree + ); - const workspaceJson = readJsonInTree(tree, '/workspace.json'); + const workspaceJson = readJsonInTree(tree, '/workspace.json'); - expect( - workspaceJson.projects['my-app'].architect.build.options.webpackConfig - ).toEqual('@nrwl/react/plugins/babel'); - }); + expect( + workspaceJson.projects['my-app'].architect.build.options.webpackConfig + ).toEqual('@nrwl/react/plugins/babel'); + }); - it('should add required polyfills for core-js and regenerator', async () => { - const tree = await runSchematic( - 'app', - { name: 'myApp', babel: true }, - appTree - ); - const packageJSON = readJsonInTree(tree, 'package.json'); - const polyfillsSource = tree - .read('apps/my-app/src/polyfills.ts') - .toString(); + it('should add required polyfills for core-js and regenerator', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', babel: true }, + appTree + ); + const polyfillsSource = tree + .read('apps/my-app/src/polyfills.ts') + .toString(); - expect(packageJSON.devDependencies['core-js']).toBeDefined(); - expect(packageJSON.devDependencies['regenerator-runtime']).toBeDefined(); - expect(polyfillsSource).toContain('regenerator'); - expect(polyfillsSource).toContain('core-js'); - }); + expect(polyfillsSource).toContain('regenerator'); + expect(polyfillsSource).toContain('core-js'); }); describe('--skipWorkspaceJson', () => { diff --git a/packages/react/src/schematics/application/application.ts b/packages/react/src/schematics/application/application.ts index 94bb802782ba7..b04e9ac4b87d5 100644 --- a/packages/react/src/schematics/application/application.ts +++ b/packages/react/src/schematics/application/application.ts @@ -37,17 +37,9 @@ import { Schema } from './schema'; import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled'; import { addInitialRoutes } from '../../utils/ast-utils'; import { - babelCoreVersion, - babelLoaderVersion, - babelPluginDecoratorsVersion, - babelPluginMacrosVersion, - babelPresetEnvVersion, babelPresetReactVersion, - babelPresetTypeScriptVersion, - coreJsVersion, reactRouterDomVersion, - reactRouterVersion, - regeneratorVersion + reactRouterVersion } from '../../utils/versions'; import { assertValidStyle } from '../../utils/assertion'; import { extraEslintDependencies, reactEslintJson } from '../../utils/lint'; @@ -122,7 +114,6 @@ function addProject(options: NormalizedSchema): Rule { architect.build = { builder: '@nrwl/web:build', options: { - differentialLoading: !options.babel, // Using babel-loader will not work with differential loading for now outputPath: join(normalize('dist'), options.appProjectRoot), index: join(options.appProjectRoot, 'src/index.html'), main: join(options.appProjectRoot, `src/main.tsx`), @@ -136,7 +127,7 @@ function addProject(options: NormalizedSchema): Rule { ? [] : [join(options.appProjectRoot, `src/styles.${options.style}`)], scripts: [], - webpackConfig: options.babel ? '@nrwl/react/plugins/babel' : undefined + webpackConfig: '@nrwl/react/plugins/babel' }, configurations: { production: { @@ -265,25 +256,15 @@ function addRouting( } function addBabel(options: NormalizedSchema): Rule { - return options.babel - ? chain([ - addDepsToPackageJson( - {}, - { - '@babel/core': babelCoreVersion, - '@babel/preset-env': babelPresetEnvVersion, - '@babel/preset-react': babelPresetReactVersion, - '@babel/preset-typescript': babelPresetTypeScriptVersion, - '@babel/plugin-proposal-decorators': babelPluginDecoratorsVersion, - 'babel-loader': babelLoaderVersion, - 'babel-plugin-macros': babelPluginMacrosVersion, - 'core-js': coreJsVersion, - 'regenerator-runtime': regeneratorVersion - } - ), - addPolyfillForBabel(options) - ]) - : noop(); + return chain([ + addDepsToPackageJson( + {}, + { + '@babel/preset-react': babelPresetReactVersion + } + ), + addPolyfillForBabel(options) + ]); } function addPolyfillForBabel(options: NormalizedSchema): Rule { @@ -334,7 +315,6 @@ function setDefaults(options: NormalizedSchema): Rule { '@nrwl/react': { ...prev, application: { - babel: options.babel, style: options.style, linter: options.linter, ...jsonIdentity(prev.application) diff --git a/packages/react/src/schematics/application/schema.d.ts b/packages/react/src/schematics/application/schema.d.ts index 9d937d5b710ad..28e876d7825cc 100644 --- a/packages/react/src/schematics/application/schema.d.ts +++ b/packages/react/src/schematics/application/schema.d.ts @@ -12,6 +12,5 @@ export interface Schema { pascalCaseFiles?: boolean; classComponent?: boolean; routing?: boolean; - babel?: boolean; skipWorkspaceJson?: boolean; } diff --git a/packages/react/src/schematics/application/schema.json b/packages/react/src/schematics/application/schema.json index 624bea287fd45..c0e764ffd5736 100644 --- a/packages/react/src/schematics/application/schema.json +++ b/packages/react/src/schematics/application/schema.json @@ -114,11 +114,6 @@ "description": "Use class components instead of functional component", "alias": "C", "default": false - }, - "babel": { - "type": "boolean", - "description": "Use Babel and TypeScript preset instead of ts-loader (Useful if you need Babel plugins)", - "default": false } }, "required": [] diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index a14f1f5909045..96866f35796eb 100644 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -8,15 +8,7 @@ export const domTypesVersion = '16.8.5'; export const reactRouterVersion = '5.0.1'; export const reactRouterDomVersion = '4.3.5'; export const testingLibraryVersion = '8.0.5'; -export const babelCoreVersion = '7.5.5'; -export const babelPresetEnvVersion = '7.5.5'; export const babelPresetReactVersion = '7.0.0'; -export const babelPresetTypeScriptVersion = '7.3.3'; -export const babelPluginDecoratorsVersion = '7.4.4'; -export const babelLoaderVersion = '8.0.6'; -export const babelPluginMacrosVersion = '2.6.1'; -export const coreJsVersion = '3.1.4'; -export const regeneratorVersion = '0.13.3'; export const reduxStarterKitversion = '0.6.3'; export const reactReduxVersion = '7.1.1'; export const reactReduxTypesVersion = '7.1.2'; diff --git a/packages/web/migrations.json b/packages/web/migrations.json index 63001b4458891..8feabb653e92a 100644 --- a/packages/web/migrations.json +++ b/packages/web/migrations.json @@ -1,3 +1,9 @@ { - "schematics": {} + "schematics": { + "update-builder-8.5.0": { + "version": "8.5.0-beta.1", + "description": "Update web build builder", + "factory": "./src/migrations/update-8-5-0/update-builder-8-5-0" + } + } } diff --git a/packages/web/package.json b/packages/web/package.json index e0925a1a91325..3e9de084dcf0b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -41,14 +41,23 @@ "@angular-devkit/build-webpack": "0.801.1", "@angular-devkit/core": "8.1.1", "@angular-devkit/schematics": "8.1.1", - "copy-webpack-plugin": "4.6.0", - "fork-ts-checker-webpack-plugin": "0.4.9", + "@babel/core": "7.5.5", + "@babel/preset-env": "7.5.5", + "@babel/preset-typescript": "7.3.3", + "@babel/plugin-proposal-class-properties": "7.5.5", + "@babel/plugin-proposal-decorators": "7.4.4", + "babel-loader": "8.0.6", + "babel-plugin-macros": "2.6.1", + "copy-webpack-plugin": "5.0.3", + "core-js": "^3.2.1", + "fork-ts-checker-webpack-plugin": "0.4.15", "identity-obj-proxy": "3.0.0", "license-webpack-plugin": "^1.4.0", - "source-map-support": "0.5.11", - "ts-loader": "5.3.1", + "regenerator-runtime": "0.13.3", + "source-map-support": "0.5.12", + "ts-loader": "5.4.5", "tsconfig-paths-webpack-plugin": "3.2.0", - "webpack": "4.29.0", + "webpack": "4.39.3", "webpack-dev-server": "3.1.14", "webpack-node-externals": "1.7.2" } diff --git a/packages/web/src/builders/build/build.impl.spec.ts b/packages/web/src/builders/build/build.impl.spec.ts index e4472ad5ee8af..be7213f80e887 100644 --- a/packages/web/src/builders/build/build.impl.spec.ts +++ b/packages/web/src/builders/build/build.impl.spec.ts @@ -19,9 +19,9 @@ describe('WebBuildBuilder', () => { context = await getMockContext(); testOptions = { index: 'apps/webapp/src/index.html', - differentialLoading: true, budgets: [], baseHref: '/', + optimization: true, deployUrl: '/', scripts: ['apps/webapp/src/scripts.js'], styles: ['apps/webapp/src/styles.css'], @@ -104,7 +104,7 @@ describe('WebBuildBuilder', () => { expect(writeIndexHtml).toHaveBeenCalled(); }); - describe('differentialLoading', () => { + describe('differential loading', () => { it('should call runWebpack twice', async () => { await run(testOptions, context).toPromise(); @@ -123,7 +123,6 @@ describe('WebBuildBuilder', () => { await run( { ...testOptions, - differentialLoading: false, webpackConfig: './apps/webapp/webpack.config.js' }, context diff --git a/packages/web/src/builders/build/build.impl.ts b/packages/web/src/builders/build/build.impl.ts index fb95745849a13..f74074f018201 100644 --- a/packages/web/src/builders/build/build.impl.ts +++ b/packages/web/src/builders/build/build.impl.ts @@ -1,23 +1,22 @@ import { BuilderContext, createBuilder } from '@angular-devkit/architect'; import { + join as devkitJoin, JsonObject, - normalize, - join as devkitJoin + normalize } from '@angular-devkit/core'; -import { runWebpack, BuildResult } from '@angular-devkit/build-webpack'; - -import { Observable, from, of, forkJoin } from 'rxjs'; +import { BuildResult, runWebpack } from '@angular-devkit/build-webpack'; +import { from, Observable, of } from 'rxjs'; import { normalizeWebBuildOptions } from '../../utils/normalize'; import { getWebConfig } from '../../utils/web.config'; import { BuildBuilderOptions } from '../../utils/types'; -import { concatMap, map, switchMap } from 'rxjs/operators'; +import { bufferCount, map, mergeScan, switchMap } from 'rxjs/operators'; import { getSourceRoot } from '../../utils/source-root'; -import { ScriptTarget } from 'typescript'; import { writeIndexHtml } from '@angular-devkit/build-angular/src/angular-cli-files/utilities/index-file/write-index-html'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { execSync } from 'child_process'; +import { Range, satisfies } from 'semver'; export interface WebBuildBuilderOptions extends BuildBuilderOptions { - differentialLoading: boolean; index: string; budgets: any[]; baseHref: string; @@ -32,7 +31,6 @@ export interface WebBuildBuilderOptions extends BuildBuilderOptions { vendorChunk?: boolean; commonChunk?: boolean; - outputHashing?: any; stylePreprocessingOptions?: any; subresourceIntegrity?: boolean; @@ -46,91 +44,118 @@ export function run( context: BuilderContext ): Observable { const host = new NodeJsSyncHost(); - return from(getSourceRoot(context, host)).pipe( - map(sourceRoot => { - options = normalizeWebBuildOptions( - options, - context.workspaceRoot, - sourceRoot - ); - let configs = [ - getWebConfig( - context.workspaceRoot, - sourceRoot, + const isScriptOptimizeOn = + typeof options.optimization === 'boolean' + ? options.optimization + : options.optimization && options.optimization.scripts + ? options.optimization.scripts + : false; + + // Node versions 12.2-12.8 has a bug where prod builds will hang for 2-3 minutes + // after the program exits. + const nodeVersion = execSync(`node --version`) + .toString('utf-8') + .trim(); + const supportedRange = new Range('10 || >=12.9'); + if (!satisfies(nodeVersion, supportedRange)) { + throw new Error( + `Node version ${nodeVersion} is not supported. Supported range is "${ + supportedRange.raw + }".` + ); + } + return from(getSourceRoot(context, host)) + .pipe( + map(sourceRoot => { + options = normalizeWebBuildOptions( options, - context.logger, - options.differentialLoading ? ScriptTarget.ES2015 : null - ) - ]; - if (options.differentialLoading) { - configs.push( + context.workspaceRoot, + sourceRoot + ); + return [ + // ESM build for modern browsers. getWebConfig( context.workspaceRoot, sourceRoot, options, context.logger, - ScriptTarget.ES5 - ) - ); - } - if (options.webpackConfig) { - configs = configs.map(config => - require(options.webpackConfig)(config, { - options, - configuration: context.target.configuration - }) - ); - } - return configs; - }), - concatMap(configs => { - return forkJoin( - configs.map(config => - runWebpack(config, context, { - logging: stats => { - context.logger.info(stats.toString(config.stats)); - } - }) - ) - ).pipe( - switchMap( - ([result1, result2 = { success: true, emittedFiles: [] }]) => { - const success = [result1, result2].every(result => result.success); - - return (options.differentialLoading - ? writeIndexHtml({ - host, - outputPath: normalize(options.outputPath), - indexPath: devkitJoin( - normalize(context.workspaceRoot), - options.index - ), - files: result1.emittedFiles.filter( - x => x.extension === '.css' - ), - noModuleFiles: result2.emittedFiles, - moduleFiles: result1.emittedFiles, - baseHref: options.baseHref, - deployUrl: options.deployUrl, - scripts: options.scripts, - styles: options.styles - }) - : of(null) - ).pipe( - map( - () => - ({ - success, - emittedFiles: [ - ...result1.emittedFiles, - ...result2.emittedFiles - ] - } as BuildResult) + true, + isScriptOptimizeOn + ), + // ES5 build for legacy browsers. + isScriptOptimizeOn + ? getWebConfig( + context.workspaceRoot, + sourceRoot, + options, + context.logger, + false, + isScriptOptimizeOn ) - ); - } + : undefined + ] + .filter(Boolean) + .map(config => + options.webpackConfig + ? require(options.webpackConfig)(config, { + options, + configuration: context.target.configuration + }) + : config + ); + }) + ) + .pipe( + switchMap(configs => + from(configs).pipe( + // Run build sequentially and bail when first one fails. + mergeScan( + (acc, config) => { + if (acc.success) { + return runWebpack(config, context, { + logging: stats => { + context.logger.info(stats.toString(config.stats)); + } + }); + } else { + return of(); + } + }, + { success: true } as BuildResult, + 1 + ), + // Collect build results as an array. + bufferCount(configs.length) ) - ); - }) - ); + ), + switchMap(([result1, result2 = { success: true, emittedFiles: [] }]) => { + const success = [result1, result2].every(result => result.success); + return (options.optimization + ? writeIndexHtml({ + host, + outputPath: normalize(options.outputPath), + indexPath: devkitJoin( + normalize(context.workspaceRoot), + options.index + ), + files: result1.emittedFiles.filter(x => x.extension === '.css'), + noModuleFiles: result2.emittedFiles, + moduleFiles: result1.emittedFiles, + baseHref: options.baseHref, + deployUrl: options.deployUrl, + scripts: options.scripts, + styles: options.styles + }) + : of(null) + ).pipe( + map( + () => + ({ + success, + emittedFiles: [...result1.emittedFiles, ...result2.emittedFiles] + } as BuildResult) + ) + ); + }) + ); } diff --git a/packages/web/src/builders/build/schema.json b/packages/web/src/builders/build/schema.json index 4b338fd888fe0..4010c37f36e54 100644 --- a/packages/web/src/builders/build/schema.json +++ b/packages/web/src/builders/build/schema.json @@ -159,11 +159,6 @@ } ] }, - "differentialLoading": { - "type": "boolean", - "description": "Enable differential loading for es5 browsers", - "default": true - }, "extractCss": { "type": "boolean", "description": "Extract css into a .css file", diff --git a/packages/web/src/builders/dev-server/dev-server.impl.ts b/packages/web/src/builders/dev-server/dev-server.impl.ts index 64642cf0a0485..aedd84c357f5d 100644 --- a/packages/web/src/builders/dev-server/dev-server.impl.ts +++ b/packages/web/src/builders/dev-server/dev-server.impl.ts @@ -44,9 +44,7 @@ function run( ): Observable { const host = new NodeJsSyncHost(); return forkJoin( - getBuildOptions(serveOptions, context, { - differentialLoading: false - }), + getBuildOptions(serveOptions, context), from(getSourceRoot(context, host)) ).pipe( map(([buildOptions, sourceRoot]) => { diff --git a/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.spec.ts b/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.spec.ts new file mode 100644 index 0000000000000..1a02fb64aaf0a --- /dev/null +++ b/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.spec.ts @@ -0,0 +1,54 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { + updateJsonInTree, + readJsonInTree, + updateWorkspaceInTree, + readWorkspace, + getWorkspacePath +} from '@nrwl/workspace'; + +import * as path from 'path'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; + +describe('Update 8-5-0', () => { + let tree: Tree; + let schematicRunner: SchematicTestRunner; + + beforeEach(async () => { + tree = Tree.empty(); + schematicRunner = new SchematicTestRunner( + '@nrwl/web', + path.join(__dirname, '../../../migrations.json') + ); + }); + + it(`should remove differentialLoading as an option for build builder`, async () => { + tree.create( + 'workspace.json', + JSON.stringify({ + projects: { + demo: { + root: 'apps/demo', + sourceRoot: 'apps/demo/src', + architect: { + build: { + builder: '@nrwl/web:build', + options: { + differentialLoading: true + } + } + } + } + } + }) + ); + + tree = await schematicRunner + .runSchematicAsync('update-builder-8.5.0', {}, tree) + .toPromise(); + + const config = readWorkspace(tree); + expect(config.projects.demo.architect.build.options).toEqual({}); + }); +}); diff --git a/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.ts b/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.ts new file mode 100644 index 0000000000000..077081dc05dcd --- /dev/null +++ b/packages/web/src/migrations/update-8-5-0/update-builder-8-5-0.ts @@ -0,0 +1,21 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updateWorkspaceInTree } from '@nrwl/workspace'; + +export default function update(): Rule { + return updateWorkspaceInTree(config => { + const filteredProjects = []; + Object.keys(config.projects).forEach(name => { + if ( + config.projects[name].architect && + config.projects[name].architect.build && + config.projects[name].architect.build.builder === '@nrwl/web:build' + ) { + filteredProjects.push(config.projects[name]); + } + }); + filteredProjects.forEach(p => { + delete p.architect.build.options.differentialLoading; + }); + return config; + }); +} diff --git a/packages/web/src/utils/config.spec.ts b/packages/web/src/utils/config.spec.ts index 5575c07ea0e75..29103809ab4a1 100644 --- a/packages/web/src/utils/config.spec.ts +++ b/packages/web/src/utils/config.spec.ts @@ -1,7 +1,6 @@ import { getBaseWebpackPartial } from './config'; import * as ts from 'typescript'; -import { ScriptTarget } from 'typescript'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import { ProgressPlugin } from 'webpack'; @@ -31,7 +30,7 @@ describe('getBaseWebpackPartial', () => { it('should have output filename', () => { const result = getBaseWebpackPartial(input); - expect(result.output.filename).toEqual('main.js'); + expect(result.output.filename).toEqual('[name].js'); }); it('should have output path', () => { @@ -43,12 +42,12 @@ describe('getBaseWebpackPartial', () => { it('should have a rule for typescript', () => { const result = getBaseWebpackPartial(input); - const typescriptRule = result.module.rules.find(rule => + const rule = result.module.rules.find(rule => (rule.test as RegExp).test('app/main.ts') ); - expect(typescriptRule).toBeTruthy(); + expect(rule).toBeTruthy(); - expect(typescriptRule.loader).toEqual('ts-loader'); + expect(rule.loader).toEqual('babel-loader'); }); it('should split typescript type checking into a separate workers', () => { @@ -128,19 +127,6 @@ describe('getBaseWebpackPartial', () => { }); describe('the tsConfig option', () => { - it('should set the correct typescript rule', () => { - const result = getBaseWebpackPartial(input); - - expect( - result.module.rules.find(rule => rule.loader === 'ts-loader').options - ).toEqual({ - configFile: 'tsconfig.json', - transpileOnly: true, - experimentalWatchApi: true, - compilerOptions: null - }); - }); - it('should set the correct options for the type checker plugin', () => { const result = getBaseWebpackPartial(input); @@ -167,37 +153,31 @@ describe('getBaseWebpackPartial', () => { }); it('should include es2015 in mainFields if typescript is set es2015', () => { - spyOn(ts, 'parseJsonConfigFileContent').and.returnValue({ - options: { - target: 'es2015' - } - }); - - const result = getBaseWebpackPartial(input); + const result = getBaseWebpackPartial(input, true); expect(result.resolve.mainFields).toContain('es2015'); }); }); - describe('script overrides', () => { - it('should override the compiler options target for es2015', () => { - const result = getBaseWebpackPartial(input, ScriptTarget.ES2015); + describe('ES modules', () => { + it('should override preset-env target for esm', () => { + const result = getBaseWebpackPartial(input, true); expect( - (result.module.rules.find(rule => rule.loader === 'ts-loader') - .options as any).compilerOptions - ).toEqual({ - target: 'es2015' + (result.module.rules.find(rule => rule.loader === 'babel-loader') + .options as any).presets.find(p => p[0] === '@babel/preset-env')[1] + ).toMatchObject({ + targets: { esmodules: true } }); }); - it('should override the compiler options target for es5', () => { - const result = getBaseWebpackPartial(input, ScriptTarget.ES5); + it('should not override preset-env target for es5', () => { + const result = getBaseWebpackPartial(input, false); expect( - (result.module.rules.find(rule => rule.loader === 'ts-loader') - .options as any).compilerOptions - ).toEqual({ - target: 'es5' + (result.module.rules.find(rule => rule.loader === 'babel-loader') + .options as any).presets.find(p => p[0] === '@babel/preset-env')[1] + ).toMatchObject({ + targets: undefined }); }); }); @@ -266,7 +246,7 @@ describe('getBaseWebpackPartial', () => { }); }); - describe('the optimization option', () => { + describe('script optimization', () => { describe('by default', () => { it('should set the mode to development', () => { const result = getBaseWebpackPartial(input); @@ -277,10 +257,7 @@ describe('getBaseWebpackPartial', () => { describe('when true', () => { it('should set the mode to production', () => { - const result = getBaseWebpackPartial({ - ...input, - optimization: true - }); + const result = getBaseWebpackPartial(input, true, true); expect(result.mode).toEqual('production'); }); diff --git a/packages/web/src/utils/config.ts b/packages/web/src/utils/config.ts index 5711250728225..357acc0ecb788 100644 --- a/packages/web/src/utils/config.ts +++ b/packages/web/src/utils/config.ts @@ -1,65 +1,86 @@ import * as webpack from 'webpack'; -import { Configuration, ProgressPlugin } from 'webpack'; - -import * as ts from 'typescript'; -import { ScriptTarget } from 'typescript'; - +import { Configuration, ProgressPlugin, Stats } from 'webpack'; +import { dirname } from 'path'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; -import CircularDependencyPlugin = require('circular-dependency-plugin'); -import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); import * as CopyWebpackPlugin from 'copy-webpack-plugin'; - -import { readTsConfig } from '@nrwl/workspace'; -import { BuildBuilderOptions } from './types'; import * as TerserWebpackPlugin from 'terser-webpack-plugin'; import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; -import { Stats } from 'webpack'; -export const OUT_FILENAME = 'main.js'; +import { BuildBuilderOptions } from './types'; +import CircularDependencyPlugin = require('circular-dependency-plugin'); +import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +import { getOutputHashFormat } from './hash-format'; export function getBaseWebpackPartial( options: BuildBuilderOptions, - overrideScriptTarget?: ScriptTarget + esm?: boolean, + isScriptOptimizeOn?: boolean ): Configuration { - const { options: compilerOptions } = readTsConfig(options.tsConfig); - const supportsEs2015 = - compilerOptions.target !== ts.ScriptTarget.ES3 && - compilerOptions.target !== ts.ScriptTarget.ES5; const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; - const mainFields = [...(supportsEs2015 ? ['es2015'] : []), 'module', 'main']; - const compilerOptionOverrides = overrideScriptTarget - ? { - target: overrideScriptTarget === ScriptTarget.ES5 ? 'es5' : 'es2015' - } - : null; + const mainFields = [...(esm ? ['es2015'] : []), 'module', 'main']; + const hashFormat = getOutputHashFormat(options.outputHashing); + const suffixFormat = esm ? '.esm' : '.es5'; + const filename = isScriptOptimizeOn + ? `[name]${hashFormat.script}${suffixFormat}.js` + : '[name].js'; + const chunkFilename = isScriptOptimizeOn + ? `[name]${hashFormat.chunk}${suffixFormat}.js` + : '[name].js'; + const webpackConfig: Configuration = { entry: { main: [options.main] }, devtool: options.sourceMap ? 'source-map' : false, - mode: options.optimization ? 'production' : 'development', + mode: isScriptOptimizeOn ? 'production' : 'development', output: { path: options.outputPath, - filename: OUT_FILENAME + filename, + chunkFilename }, module: { rules: [ { - test: /\.(j|t)sx?$/, - loader: `ts-loader`, + test: /\.([jt])sx?$/, + loader: `babel-loader`, + exclude: /node_modules/, options: { - configFile: options.tsConfig, - transpileOnly: true, - // https://github.com/TypeStrong/ts-loader/pull/685 - experimentalWatchApi: true, - compilerOptions: compilerOptionOverrides + cacheDirectory: true, + cacheCompression: false, + compact: isScriptOptimizeOn, + presets: [ + [ + '@babel/preset-env', + { + // Allows browserlist file from project to be used. + configPath: dirname(options.main), + // Allow importing core-js in entrypoint and use browserlist to select polyfills. + // This is needed for differential loading as well. + useBuiltIns: 'entry', + debug: options.verbose, + corejs: 3, + modules: false, + // Exclude transforms that make all code slower + exclude: ['transform-typeof-symbol'], + // Let babel-env figure which modern browsers to support. + // See: https://github.com/babel/babel/blob/master/packages/babel-preset-env/data/built-in-modules.json + targets: esm ? { esmodules: true } : undefined + } + ], + ['@babel/preset-typescript'] + ], + plugins: [ + 'babel-plugin-macros', + ['@babel/plugin-proposal-class-properties'], + ['@babel/plugin-proposal-decorators', false] + ] } } ] }, resolve: { extensions, - alias: getAliases(options, compilerOptions), + alias: getAliases(options), plugins: [ new TsConfigPathsPlugin({ configFile: options.tsConfig, @@ -85,9 +106,10 @@ export function getBaseWebpackPartial( stats: getStatsConfig(options) }; - if (options.optimization) { + if (isScriptOptimizeOn) { webpackConfig.optimization = { - minimizer: [createTerserPlugin(compilerOptions.target)] + minimizer: [createTerserPlugin(esm)], + runtimeChunk: true }; } @@ -147,10 +169,7 @@ export function getBaseWebpackPartial( return webpackConfig; } -function getAliases( - options: BuildBuilderOptions, - compilerOptions: ts.CompilerOptions -): { [key: string]: string } { +function getAliases(options: BuildBuilderOptions): { [key: string]: string } { return options.fileReplacements.reduce( (aliases, replacement) => ({ ...aliases, @@ -160,15 +179,12 @@ function getAliases( ); } -export function createTerserPlugin(scriptTarget: ScriptTarget) { +export function createTerserPlugin(esm: boolean) { return new TerserWebpackPlugin({ parallel: true, cache: true, terserOptions: { - ecma: - scriptTarget === ScriptTarget.ES3 || scriptTarget === ScriptTarget.ES5 - ? 5 - : 6, + ecma: esm ? 6 : 5, safari10: true, output: { ascii_only: true, @@ -201,3 +217,15 @@ function getStatsConfig(options: BuildBuilderOptions): Stats.ToStringOptions { usedExports: !!options.verbose }; } + +function createFileName(esm, isScriptOptimizeOn, outputHashing) { + return isScriptOptimizeOn + ? esm + ? outputHashing + ? '[name].[chunkhash].esm.js' + : '[name].esm.js' + : outputHashing + ? '[name].[chunkhash].es5.js' + : '[name].es5.js' + : '[name].js'; +} diff --git a/packages/web/src/utils/devserver.config.spec.ts b/packages/web/src/utils/devserver.config.spec.ts index 8a57b12dfd636..ca9ed21210787 100644 --- a/packages/web/src/utils/devserver.config.spec.ts +++ b/packages/web/src/utils/devserver.config.spec.ts @@ -19,7 +19,6 @@ describe('getDevServerConfig', () => { beforeEach(() => { buildInput = { main: 'main.ts', - differentialLoading: true, index: 'index.html', budgets: [], baseHref: '/', diff --git a/packages/web/src/utils/devserver.config.ts b/packages/web/src/utils/devserver.config.ts index 20e4783a4e4b4..03811dc5bcea0 100644 --- a/packages/web/src/utils/devserver.config.ts +++ b/packages/web/src/utils/devserver.config.ts @@ -24,7 +24,9 @@ export function getDevServerConfig( root, sourceRoot, buildOptions, - logger + logger, + true, // Don't need to support legacy browsers for dev. + false ); (webpackConfig as any).devServer = getDevServerPartial( root, diff --git a/packages/web/src/utils/hash-format.ts b/packages/web/src/utils/hash-format.ts new file mode 100644 index 0000000000000..08668e32acfaf --- /dev/null +++ b/packages/web/src/utils/hash-format.ts @@ -0,0 +1,28 @@ +// Originally from devkit. +// See: https://github.com/angular/angular-cli/blob/2c8b12f/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts +export interface HashFormat { + chunk: string; + extract: string; + file: string; + script: string; +} + +export function getOutputHashFormat(option: string, length = 20): HashFormat { + const hashFormats: { [option: string]: HashFormat } = { + none: { chunk: '', extract: '', file: '', script: '' }, + media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' }, + bundles: { + chunk: `.[chunkhash:${length}]`, + extract: `.[contenthash:${length}]`, + file: '', + script: `.[hash:${length}]` + }, + all: { + chunk: `.[chunkhash:${length}]`, + extract: `.[contenthash:${length}]`, + file: `.[hash:${length}]`, + script: `.[hash:${length}]` + } + }; + return hashFormats[option] || hashFormats['none']; +} diff --git a/packages/web/src/utils/normalize.ts b/packages/web/src/utils/normalize.ts index 86564a79c7dec..a1d410ce842b6 100644 --- a/packages/web/src/utils/normalize.ts +++ b/packages/web/src/utils/normalize.ts @@ -118,17 +118,12 @@ export function normalizeWebBuildOptions( }; } -export function convertBuildOptions( - buildOptions: WebBuildBuilderOptions, - scriptTargetOverride: ScriptTarget -): any { +export function convertBuildOptions(buildOptions: WebBuildBuilderOptions): any { const options = buildOptions as any; return { ...options, buildOptimizer: options.optimization, aot: false, - scriptTargetOverride: scriptTargetOverride, - esVersionInFileName: !!scriptTargetOverride, forkTypeChecker: false, lazyModules: [] as string[], assets: [] as string[] diff --git a/packages/web/src/utils/types.ts b/packages/web/src/utils/types.ts index c8b967c433e9b..6ddbf784d7287 100644 --- a/packages/web/src/utils/types.ts +++ b/packages/web/src/utils/types.ts @@ -32,6 +32,7 @@ export interface BuildBuilderOptions { extractLicenses?: boolean; verbose?: boolean; + outputHashing?: any; webpackConfig?: string; root?: string; diff --git a/packages/web/src/utils/web.config.spec.ts b/packages/web/src/utils/web.config.spec.ts index 95a54cd84129f..00ccd5a903cb6 100644 --- a/packages/web/src/utils/web.config.spec.ts +++ b/packages/web/src/utils/web.config.spec.ts @@ -4,7 +4,6 @@ import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import { createConsoleLogger } from '@angular-devkit/core/node'; import { Logger } from '@angular-devkit/core/src/logger'; import * as ts from 'typescript'; -import { ScriptTarget } from 'typescript'; import { WebBuildBuilderOptions } from '../builders/build/build.impl'; import { join } from 'path'; @@ -32,7 +31,6 @@ describe('getWebConfig', () => { scripts: false, styles: false }, - differentialLoading: true, styles: [], scripts: [], outputPath: 'dist', @@ -59,12 +57,12 @@ describe('getWebConfig', () => { }); it('should resolve the browser main field', () => { - const result = getWebPartial(root, sourceRoot, input, logger); + const result = getWebPartial(root, sourceRoot, input, logger, false, false); expect(result.resolve.mainFields).toContain('browser'); }); it('should use the style-loader to load styles', () => { - const result = getWebPartial(root, sourceRoot, input, logger); + const result = getWebPartial(root, sourceRoot, input, logger, false, false); expect( result.module.rules.find(rule => rule.test.test('styles.css')).use[0] .loader @@ -85,7 +83,9 @@ describe('getWebConfig', () => { ...input, polyfills: 'polyfills.ts' }, - logger + logger, + false, + false ); expect(result.entry.polyfills).toEqual(['polyfills.ts']); }); @@ -98,10 +98,11 @@ describe('getWebConfig', () => { sourceRoot, { ...input, - differentialLoading: false, es2015Polyfills: 'polyfills.es2015.ts' }, - logger + logger, + false, + false ); expect(result.entry['polyfills-es5']).toEqual(['polyfills.es2015.ts']); }); @@ -116,11 +117,11 @@ describe('getWebConfig', () => { sourceRoot, { ...input, - differentialLoading: false, polyfills: 'polyfills.ts' }, logger, - ScriptTarget.ES2015 + true, + true ); expect(es2015Config.entry.polyfills).toContain('polyfills.ts'); const es5Config = getWebPartial( @@ -131,7 +132,8 @@ describe('getWebConfig', () => { polyfills: 'polyfills.ts' }, logger, - ScriptTarget.ES5 + false, + true ); expect(es5Config.entry.polyfills).toContain('polyfills.ts'); }); @@ -148,7 +150,8 @@ describe('getWebConfig', () => { es2015Polyfills: 'polyfills.es2015.ts' }, logger, - ScriptTarget.ES5 + false, + true ); expect(es5Config.entry.polyfills).toContain('polyfills.es2015.ts'); }); @@ -164,7 +167,8 @@ describe('getWebConfig', () => { polyfills: 'polyfills.ts' }, logger, - ScriptTarget.ES2015 + true, + true ); expect(es2015Config.entry.polyfills).toContain( require.resolve( diff --git a/packages/web/src/utils/web.config.ts b/packages/web/src/utils/web.config.ts index 9b16d6489e836..ca19ce6fe1b48 100644 --- a/packages/web/src/utils/web.config.ts +++ b/packages/web/src/utils/web.config.ts @@ -5,62 +5,49 @@ import { getCommonConfig } from '@angular-devkit/build-angular/src/angular-cli-f import { getStylesConfig } from '@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/styles'; import { Configuration } from 'webpack'; import { LoggerApi } from '@angular-devkit/core/src/logger'; -import { resolve, basename } from 'path'; +import { basename, resolve } from 'path'; import { WebBuildBuilderOptions } from '../builders/build/build.impl'; import { convertBuildOptions } from './normalize'; import { readTsConfig } from '@nrwl/workspace'; import { getBaseWebpackPartial } from './config'; import { IndexHtmlWebpackPlugin } from '@angular-devkit/build-angular/src/angular-cli-files/plugins/index-html-webpack-plugin'; import { generateEntryPoints } from '@angular-devkit/build-angular/src/angular-cli-files/utilities/package-chunk-sort'; -import { ScriptTarget } from 'typescript'; export function getWebConfig( root, sourceRoot, options: WebBuildBuilderOptions, logger: LoggerApi, - overrideScriptTarget?: ScriptTarget + esm?: boolean, + isScriptOptimizeOn?: boolean ) { const tsConfig = readTsConfig(options.tsConfig); - - if ( - options.differentialLoading && - !( - tsConfig.options.target !== ScriptTarget.ES5 && - tsConfig.options.target !== ScriptTarget.ES3 - ) - ) { - const message = `Differential Loading is only necessary for targeting ES2015 and above. To target ES2015, set the target field in your tsconfig.json to 'es2015'`; - logger.fatal(message); - throw new Error(message); - } - - const supportES2015 = options.differentialLoading - ? overrideScriptTarget === ScriptTarget.ES2015 - : tsConfig.options.target !== ScriptTarget.ES5 && - tsConfig.options.target !== ScriptTarget.ES3; const wco: any = { root, projectRoot: resolve(root, sourceRoot), - buildOptions: convertBuildOptions(options, overrideScriptTarget), - supportES2015, + buildOptions: convertBuildOptions(options), + esm, logger, tsConfig, tsConfigPath: options.tsConfig }; return mergeWebpack([ - _getBaseWebpackPartial(options, overrideScriptTarget), - getPolyfillsPartial(options, overrideScriptTarget), + _getBaseWebpackPartial(options, esm, isScriptOptimizeOn), + getPolyfillsPartial(options, esm, isScriptOptimizeOn), getStylesPartial(wco), getCommonPartial(wco), - getBrowserPartial(wco, options) + getBrowserPartial(wco, options, isScriptOptimizeOn) ]); } -function getBrowserPartial(wco: any, options: WebBuildBuilderOptions) { +function getBrowserPartial( + wco: any, + options: WebBuildBuilderOptions, + isScriptOptimizeOn: boolean +) { const config = getBrowserConfig(wco); - if (!wco.buildOptions.differentialLoading) { + if (!isScriptOptimizeOn) { const { deployUrl, subresourceIntegrity, @@ -88,9 +75,10 @@ function getBrowserPartial(wco: any, options: WebBuildBuilderOptions) { function _getBaseWebpackPartial( options: WebBuildBuilderOptions, - overrideScriptTarget: ScriptTarget + esm: boolean, + isScriptOptimizeOn: boolean ) { - let partial = getBaseWebpackPartial(options, overrideScriptTarget); + let partial = getBaseWebpackPartial(options, esm, isScriptOptimizeOn); delete partial.resolve.mainFields; return partial; } @@ -130,28 +118,23 @@ function getStylesPartial(wco: any): Configuration { function getPolyfillsPartial( options: WebBuildBuilderOptions, - overrideScriptTarget: ScriptTarget + esm: boolean, + isScriptOptimizeOn: boolean ): Configuration { const config = { entry: {} as { [key: string]: string[] } }; - if ( - options.polyfills && - options.differentialLoading && - overrideScriptTarget === ScriptTarget.ES2015 - ) { + if (options.polyfills && esm && isScriptOptimizeOn) { + // Safari 10.1 supports