From 701214d174586fe7373b6155024c9b6e97b26377 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 29 Jul 2021 19:38:16 -0400 Subject: [PATCH] feat(@angular-devkit/build-angular): remove differential loading support BREAKING CHANGE: Differential loading support has been removed. With Angular no longer supporting IE11, there are now no browsers officially supported by Angular that require ES5 code. As a result, differential loading's functionality for creating and conditionally loading ES5 and ES2015+ variants of an application is no longer required. --- .../build_angular/src/browser/index.ts | 407 ++---------------- .../src/browser/specs/browser-support_spec.ts | 3 - .../specs/differential_loading_spec.ts | 196 --------- .../src/browser/specs/scripts-array_spec.ts | 34 -- .../src/browser/specs/styles_spec.ts | 12 - .../build_angular/src/dev-server/index.ts | 16 +- .../src/dev-server/index_spec.ts | 89 ---- .../build_angular/src/utils/action-cache.ts | 187 -------- .../src/utils/action-executor.ts | 36 +- .../src/utils/build-browser-features.ts | 10 - .../src/utils/build-browser-features_spec.ts | 38 -- .../build_angular/src/utils/build-options.ts | 2 - .../src/utils/bundle-calculator.ts | 182 ++------ .../src/utils/bundle-calculator_spec.ts | 407 ++---------------- .../build_angular/src/utils/process-bundle.ts | 370 +--------------- .../src/webpack/configs/browser.ts | 7 +- .../src/webpack/configs/common.ts | 35 +- tests/legacy-cli/e2e/tests/basic/build.ts | 16 +- .../e2e/tests/basic/ngcc-es2015-only.ts | 4 +- .../e2e/tests/build/differential-cache.ts | 88 ---- .../tests/build/differential-loading-sri.ts | 67 --- .../tests/build/differential-loading-watch.ts | 15 - .../e2e/tests/build/differential-loading.ts | 53 --- tests/legacy-cli/e2e/tests/build/polyfills.ts | 13 +- .../legacy-cli/e2e/tests/build/prod-build.ts | 20 +- tests/legacy-cli/e2e/tests/build/sourcemap.ts | 12 +- tests/legacy-cli/e2e/tests/build/worker.ts | 15 +- .../e2e/tests/i18n/ivy-localize-dl-arb.ts | 10 - .../e2e/tests/i18n/ivy-localize-dl-json.ts | 10 - .../e2e/tests/i18n/ivy-localize-dl-xliff1.ts | 10 - .../e2e/tests/i18n/ivy-localize-dl-xliff2.ts | 106 ----- .../e2e/tests/i18n/ivy-localize-dl-xmb.ts | 10 - .../e2e/tests/i18n/ivy-localize-es5.ts | 7 +- tests/legacy-cli/e2e/tests/update/update-8.ts | 5 +- 34 files changed, 135 insertions(+), 2357 deletions(-) delete mode 100644 packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts delete mode 100644 packages/angular_devkit/build_angular/src/utils/action-cache.ts delete mode 100644 tests/legacy-cli/e2e/tests/build/differential-cache.ts delete mode 100644 tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts delete mode 100644 tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts delete mode 100644 tests/legacy-cli/e2e/tests/build/differential-loading.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-arb.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-json.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff1.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts delete mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xmb.ts diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index 6b247bedd99d..3374e9a52264 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -21,27 +21,17 @@ import { deleteOutputDir, normalizeAssetPatterns, normalizeOptimization, - normalizeSourceMaps, urlJoin, } from '../utils'; -import { BundleActionExecutor } from '../utils/action-executor'; import { ThresholdSeverity, checkBudgets } from '../utils/bundle-calculator'; -import { findCachePath } from '../utils/cache-path'; import { colors } from '../utils/color'; import { copyAssets } from '../utils/copy-assets'; -import { cachingDisabled } from '../utils/environment-options'; import { i18nInlineEmittedFiles } from '../utils/i18n-inlining'; import { I18nOptions } from '../utils/i18n-options'; import { FileInfo } from '../utils/index-file/augment-index-html'; import { IndexHtmlGenerator, IndexHtmlTransform } from '../utils/index-file/index-html-generator'; import { ensureOutputPaths } from '../utils/output-paths'; import { generateEntryPoints } from '../utils/package-chunk-sort'; -import { - InlineOptions, - ProcessBundleFile, - ProcessBundleOptions, - ProcessBundleResult, -} from '../utils/process-bundle'; import { readTsconfig } from '../utils/read-tsconfig'; import { augmentAppWithServiceWorker } from '../utils/service-worker'; import { Spinner } from '../utils/spinner'; @@ -63,9 +53,6 @@ import { import { markAsyncChunksNonInitial } from '../webpack/utils/async-chunks'; import { normalizeExtraEntryPoints } from '../webpack/utils/helpers'; import { - BundleStats, - ChunkType, - generateBundleStats, statsErrorsToString, statsHasErrors, statsHasWarnings, @@ -74,8 +61,6 @@ import { } from '../webpack/utils/stats'; import { Schema as BrowserBuilderSchema } from './schema'; -const cacheDownlevelPath = cachingDisabled ? undefined : findCachePath('angular-build-dl'); - /** * @experimental Direct usage of this type is considered experimental. */ @@ -92,7 +77,6 @@ export type BrowserBuilderOutput = json.JsonObject & async function initialize( options: BrowserBuilderSchema, context: BuilderContext, - differentialLoadingNeeded: boolean, webpackConfigurationTransform?: ExecutionTransformer, ): Promise<{ config: webpack.Configuration; @@ -106,20 +90,15 @@ async function initialize( const adjustedOptions = options.watch ? options : { ...options, assets: [] }; const { config, projectRoot, projectSourceRoot, i18n } = - await generateI18nBrowserWebpackConfigFromContext( - adjustedOptions, - context, - (wco) => [ - getCommonConfig(wco), - getBrowserConfig(wco), - getStylesConfig(wco), - getStatsConfig(wco), - getAnalyticsConfig(wco, context), - getTypeScriptConfig(wco), - wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}, - ], - { differentialLoadingNeeded }, - ); + await generateI18nBrowserWebpackConfigFromContext(adjustedOptions, context, (wco) => [ + getCommonConfig(wco), + getBrowserConfig(wco), + getStylesConfig(wco), + getStatsConfig(wco), + getAnalyticsConfig(wco, context), + getTypeScriptConfig(wco), + wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}, + ]); // Validate asset option values if processed directly if (options.assets?.length && !adjustedOptions.assets?.length) { @@ -185,33 +164,18 @@ export function buildWebpackBrowser( const { options: compilerOptions } = readTsconfig(options.tsConfig, context.workspaceRoot); const target = compilerOptions.target || ScriptTarget.ES5; const buildBrowserFeatures = new BuildBrowserFeatures(sysProjectRoot); - const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(target); checkInternetExplorerSupport(buildBrowserFeatures.supportedBrowsers, context.logger); return { - ...(await initialize( - options, - context, - isDifferentialLoadingNeeded, - transforms.webpackConfiguration, - )), + ...(await initialize(options, context, transforms.webpackConfiguration)), buildBrowserFeatures, - isDifferentialLoadingNeeded, target, }; }), switchMap( // eslint-disable-next-line max-lines-per-function - ({ - config, - projectRoot, - projectSourceRoot, - i18n, - buildBrowserFeatures, - isDifferentialLoadingNeeded, - target, - }) => { + ({ config, projectRoot, projectSourceRoot, i18n, buildBrowserFeatures, target }) => { const normalizedOptimization = normalizeOptimization(options.optimization); return runWebpack(config, context, { @@ -258,338 +222,38 @@ export function buildWebpackBrowser( return { success }; } else { - const processResults: ProcessBundleResult[] = []; - const bundleInfoStats: BundleStats[] = []; outputPaths = ensureOutputPaths(baseOutputPath, i18n); - let noModuleFiles: EmittedFiles[] | undefined; let moduleFiles: EmittedFiles[] | undefined; - let files: EmittedFiles[] | undefined; const scriptsEntryPointName = normalizeExtraEntryPoints( options.scripts || [], 'scripts', ).map((x) => x.bundleName); - if (isDifferentialLoadingNeeded && options.watch) { - moduleFiles = emittedFiles; - files = moduleFiles.filter( - (x) => - x.extension === '.css' || (x.name && scriptsEntryPointName.includes(x.name)), + const files = emittedFiles.filter((x) => x.name !== 'polyfills-es5'); + const noModuleFiles = emittedFiles.filter((x) => x.name === 'polyfills-es5'); + if (i18n.shouldInline) { + const success = await i18nInlineEmittedFiles( + context, + emittedFiles, + i18n, + baseOutputPath, + Array.from(outputPaths.values()), + scriptsEntryPointName, + webpackOutputPath, + target <= ScriptTarget.ES5, + options.i18nMissingTranslation, ); - if (i18n.shouldInline) { - const success = await i18nInlineEmittedFiles( - context, - emittedFiles, - i18n, - baseOutputPath, - Array.from(outputPaths.values()), - scriptsEntryPointName, - webpackOutputPath, - target <= ScriptTarget.ES5, - options.i18nMissingTranslation, - ); - if (!success) { - return { success: false }; - } - } - } else if (isDifferentialLoadingNeeded) { - moduleFiles = []; - noModuleFiles = []; - - // Common options for all bundle process actions - const sourceMapOptions = normalizeSourceMaps(options.sourceMap || false); - const actionOptions: Partial = { - optimize: normalizedOptimization.scripts, - sourceMaps: sourceMapOptions.scripts, - hiddenSourceMaps: sourceMapOptions.hidden, - vendorSourceMaps: sourceMapOptions.vendor, - integrityAlgorithm: options.subresourceIntegrity ? 'sha384' : undefined, - }; - - let mainChunkId; - const actions: ProcessBundleOptions[] = []; - let workerReplacements: [string, string][] | undefined; - const seen = new Set(); - for (const file of emittedFiles) { - // Assets are not processed nor injected into the index - if (file.asset) { - // WorkerPlugin adds worker files to assets - if (file.file.endsWith('.worker.js')) { - if (!workerReplacements) { - workerReplacements = []; - } - workerReplacements.push([ - file.file, - file.file.replace(/\-(es20\d{2}|esnext)/, '-es5'), - ]); - } else { - continue; - } - } - - // Scripts and non-javascript files are not processed - if ( - file.extension !== '.js' || - (file.name && scriptsEntryPointName.includes(file.name)) - ) { - if (files === undefined) { - files = []; - } - files.push(file); - continue; - } - - // Ignore already processed files; emittedFiles can contain duplicates - if (seen.has(file.file)) { - continue; - } - seen.add(file.file); - - if (file.name === 'vendor' || (!mainChunkId && file.name === 'main')) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mainChunkId = file.id!.toString(); - } - - // All files at this point except ES5 polyfills are module scripts - const es5Polyfills = file.file.startsWith('polyfills-es5'); - if (!es5Polyfills) { - moduleFiles.push(file); - } - - // Retrieve the content/map for the file - // NOTE: Additional future optimizations will read directly from memory - let filename = path.join(webpackOutputPath, file.file); - const code = fs.readFileSync(filename, 'utf8'); - let map; - if (actionOptions.sourceMaps) { - try { - map = fs.readFileSync(filename + '.map', 'utf8'); - if (es5Polyfills) { - fs.unlinkSync(filename + '.map'); - } - } catch {} - } - - if (es5Polyfills) { - fs.unlinkSync(filename); - filename = filename.replace(/\-es20\d{2}/, ''); - } - - const es2015Polyfills = file.file.startsWith('polyfills-es20'); - - // Record the bundle processing action - // The runtime chunk gets special processing for lazy loaded files - actions.push({ - ...actionOptions, - filename, - code, - map, - // id is always present for non-assets - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - name: file.id!, - runtime: file.file.startsWith('runtime'), - ignoreOriginal: es5Polyfills, - optimizeOnly: es2015Polyfills, - }); - - // ES2015 polyfills are only optimized; optimization check was performed above - if (es2015Polyfills) { - continue; - } - - // Add the newly created ES5 bundles to the index as nomodule scripts - const newFilename = es5Polyfills - ? file.file.replace(/\-es20\d{2}/, '') - : file.file.replace(/\-(es20\d{2}|esnext)/, '-es5'); - noModuleFiles.push({ ...file, file: newFilename }); - } - - const processActions: typeof actions = []; - let processRuntimeAction: ProcessBundleOptions | undefined; - for (const action of actions) { - // If SRI is enabled always process the runtime bundle - // Lazy route integrity values are stored in the runtime bundle - if (action.integrityAlgorithm && action.runtime) { - processRuntimeAction = action; - } else { - processActions.push({ replacements: workerReplacements, ...action }); - } - } - - const executor = new BundleActionExecutor( - { cachePath: cacheDownlevelPath, i18n }, - options.subresourceIntegrity ? 'sha384' : undefined, - ); - - // Execute the bundle processing actions - try { - spinner.start('Generating ES5 bundles for differential loading...'); - for await (const result of executor.processAll(processActions)) { - processResults.push(result); - } - - // Runtime must be processed after all other files - if (processRuntimeAction) { - const runtimeOptions = { - ...processRuntimeAction, - runtimeData: processResults, - supportedBrowsers: buildBrowserFeatures.supportedBrowsers, - }; - processResults.push( - await import('../utils/process-bundle').then((m) => - m.process(runtimeOptions), - ), - ); - } - - spinner.succeed('ES5 bundle generation complete.'); - - if (i18n.shouldInline) { - spinner.start('Generating localized bundles...'); - const inlineActions: InlineOptions[] = []; - const processedFiles = new Set(); - for (const result of processResults) { - if (result.original) { - inlineActions.push({ - filename: path.basename(result.original.filename), - code: fs.readFileSync(result.original.filename, 'utf8'), - map: - result.original.map && - fs.readFileSync(result.original.map.filename, 'utf8'), - outputPath: baseOutputPath, - es5: false, - missingTranslation: options.i18nMissingTranslation, - setLocale: result.name === mainChunkId, - }); - processedFiles.add(result.original.filename); - if (result.original.map) { - processedFiles.add(result.original.map.filename); - } - } - if (result.downlevel) { - inlineActions.push({ - filename: path.basename(result.downlevel.filename), - code: fs.readFileSync(result.downlevel.filename, 'utf8'), - map: - result.downlevel.map && - fs.readFileSync(result.downlevel.map.filename, 'utf8'), - outputPath: baseOutputPath, - es5: true, - missingTranslation: options.i18nMissingTranslation, - setLocale: result.name === mainChunkId, - }); - processedFiles.add(result.downlevel.filename); - if (result.downlevel.map) { - processedFiles.add(result.downlevel.map.filename); - } - } - } - - let hasErrors = false; - try { - for await (const result of executor.inlineAll(inlineActions)) { - if (options.verbose) { - context.logger.info( - `Localized "${result.file}" [${result.count} translation(s)].`, - ); - } - for (const diagnostic of result.diagnostics) { - spinner.stop(); - if (diagnostic.type === 'error') { - hasErrors = true; - context.logger.error(diagnostic.message); - } else { - context.logger.warn(diagnostic.message); - } - spinner.start(); - } - } - - // Copy any non-processed files into the output locations - await copyAssets( - [ - { - glob: '**/*', - input: webpackOutputPath, - output: '', - ignore: [...processedFiles].map((f) => - path.relative(webpackOutputPath, f), - ), - }, - ], - Array.from(outputPaths.values()), - '', - ); - } catch (err) { - spinner.fail('Localized bundle generation failed.'); - - return { success: false, error: mapErrorToMessage(err) }; - } - - if (hasErrors) { - spinner.fail('Localized bundle generation failed.'); - } else { - spinner.succeed('Localized bundle generation complete.'); - } - - if (hasErrors) { - return { success: false }; - } - } - } finally { - executor.stop(); - } - for (const result of processResults) { - const chunk = webpackStats.chunks?.find( - (chunk) => chunk.id?.toString() === result.name, - ); - - if (result.original) { - bundleInfoStats.push(generateBundleInfoStats(result.original, chunk, 'modern')); - } - - if (result.downlevel) { - bundleInfoStats.push( - generateBundleInfoStats(result.downlevel, chunk, 'legacy'), - ); - } - } - - const unprocessedChunks = - webpackStats.chunks?.filter( - (chunk) => - !processResults.find((result) => chunk.id?.toString() === result.name), - ) || []; - for (const chunk of unprocessedChunks) { - const asset = webpackStats.assets?.find((a) => a.name === chunk.files?.[0]); - bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset?.size })); - } - } else { - files = emittedFiles.filter((x) => x.name !== 'polyfills-es5'); - noModuleFiles = emittedFiles.filter((x) => x.name === 'polyfills-es5'); - if (i18n.shouldInline) { - const success = await i18nInlineEmittedFiles( - context, - emittedFiles, - i18n, - baseOutputPath, - Array.from(outputPaths.values()), - scriptsEntryPointName, - webpackOutputPath, - target <= ScriptTarget.ES5, - options.i18nMissingTranslation, - ); - if (!success) { - return { success: false }; - } + if (!success) { + return { success: false }; } } // Check for budget errors and display them to the user. const budgets = options.budgets; if (budgets?.length) { - const budgetFailures = checkBudgets(budgets, webpackStats, processResults); + const budgetFailures = checkBudgets(budgets, webpackStats); for (const { severity, message } of budgetFailures) { switch (severity) { case ThresholdSeverity.Warning: @@ -710,7 +374,7 @@ export function buildWebpackBrowser( } } - webpackStatsLogger(context.logger, webpackStats, config, bundleInfoStats); + webpackStatsLogger(context.logger, webpackStats, config); return { success: buildSuccess }; } @@ -760,21 +424,6 @@ function assertNever(input: never): never { ); } -function generateBundleInfoStats( - bundle: ProcessBundleFile, - chunk: webpack.StatsChunk | undefined, - chunkType: ChunkType, -): BundleStats { - return generateBundleStats({ - size: bundle.size, - files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], - names: chunk?.names, - initial: !!chunk?.initial, - rendered: true, - chunkType, - }); -} - function mapEmittedFilesToFileInfo(files: EmittedFiles[] = []): FileInfo[] { const filteredFiles: FileInfo[] = []; for (const { file, name, extension, initial } of files) { diff --git a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts index 3d0bdeca2adb..1f41de261637 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts @@ -17,9 +17,6 @@ describe('Browser Builder browser support', () => { beforeEach(async () => { await host.initialize().toPromise(); architect = (await createArchitect(host.root())).architect; - - // target ES5 to disable differential loading which is not needed for the tests - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); }); afterEach(async () => host.restore().toPromise()); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts deleted file mode 100644 index caf617a7d95f..000000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { PathFragment } from '@angular-devkit/core'; -import { browserBuild, createArchitect, host } from '../../testing/test-utils'; - -const TEST_TIMEOUT = 8 * 60 * 1000; - -describe('Browser Builder with differential loading', () => { - const target = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - // to trigger differential loading we need an non ever green browser - host.writeMultipleFiles({ - '.browserslistrc': 'IE 10', - }); - - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(async () => host.restore().toPromise()); - - it( - 'emits all the neccessary files for default configuration', - async () => { - const { files } = await browserBuild(architect, host, target, { sourceMap: true }); - - const expectedOutputs = [ - 'favicon.ico', - 'index.html', - - 'main-es2017.js', - 'main-es2017.js.map', - 'main-es5.js', - 'main-es5.js.map', - - 'polyfills-es2017.js', - 'polyfills-es2017.js.map', - 'polyfills-es5.js', - 'polyfills-es5.js.map', - - 'runtime-es2017.js', - 'runtime-es2017.js.map', - 'runtime-es5.js', - 'runtime-es5.js.map', - - 'vendor-es2017.js', - 'vendor-es2017.js.map', - 'vendor-es5.js', - 'vendor-es5.js.map', - - 'styles.css', - 'styles.css.map', - ] as PathFragment[]; - - expect(Object.keys(files)).toEqual(jasmine.arrayWithExactContents(expectedOutputs)); - }, - TEST_TIMEOUT, - ); - - it( - 'emits all the neccessary files for target of ESNext', - async () => { - host.replaceInFile('tsconfig.json', '"target": "es2017",', `"target": "esnext",`); - - const { files } = await browserBuild(architect, host, target, { sourceMap: true }); - const expectedOutputs = [ - 'favicon.ico', - 'index.html', - - 'main-esnext.js', - 'main-esnext.js.map', - 'main-es5.js', - 'main-es5.js.map', - - 'polyfills-esnext.js', - 'polyfills-esnext.js.map', - 'polyfills-es5.js', - 'polyfills-es5.js.map', - - 'runtime-esnext.js', - 'runtime-esnext.js.map', - 'runtime-es5.js', - 'runtime-es5.js.map', - - 'vendor-esnext.js', - 'vendor-esnext.js.map', - 'vendor-es5.js', - 'vendor-es5.js.map', - - 'styles.css', - 'styles.css.map', - ] as PathFragment[]; - - expect(Object.keys(files)).toEqual(jasmine.arrayWithExactContents(expectedOutputs)); - }, - TEST_TIMEOUT, - ); - - it( - 'deactivates differential loading for watch mode', - async () => { - const { files } = await browserBuild(architect, host, target, { - watch: true, - sourceMap: true, - }); - - const expectedOutputs = [ - 'favicon.ico', - 'index.html', - - 'main-es2017.js', - 'main-es2017.js.map', - - 'polyfills-es2017.js', - 'polyfills-es2017.js.map', - - 'runtime-es2017.js', - 'runtime-es2017.js.map', - - 'vendor-es2017.js', - 'vendor-es2017.js.map', - - 'styles.css', - 'styles.css.map', - ] as PathFragment[]; - - expect(Object.keys(files)).toEqual(jasmine.arrayWithExactContents(expectedOutputs)); - }, - TEST_TIMEOUT, - ); - - it( - 'emits the right ES formats', - async () => { - const { files } = await browserBuild(architect, host, target, { - optimization: true, - vendorChunk: false, - }); - expect(await files['main-es5.js']).not.toContain('const '); - expect(await files['main-es2017.js']).toContain('const '); - }, - TEST_TIMEOUT, - ); - - it( - 'wraps ES5 scripts in an IIFE', - async () => { - const { files } = await browserBuild(architect, host, target, { optimization: false }); - expect(await files['main-es5.js']).toMatch(/^\(function \(\) \{/); - expect(await files['main-es2017.js']).not.toMatch(/^\(function \(\) \{/); - }, - TEST_TIMEOUT, - ); - - it( - 'uses the right zone.js variant', - async () => { - const { files } = await browserBuild(architect, host, target, { optimization: false }); - expect(await files['polyfills-es5.js']).toContain('zone.js/plugins/zone-legacy'); - expect(await files['polyfills-es5.js']).toContain('registerElementPatch'); - expect(await files['polyfills-es2017.js']).not.toContain('zone.js/plugins/zone-legacy'); - expect(await files['polyfills-es2017.js']).not.toContain('registerElementPatch'); - }, - TEST_TIMEOUT, - ); - - it( - 'adds `type="module"` when differential loading is needed', - async () => { - host.writeMultipleFiles({ - '.browserslistrc': ` - last 1 chrome version - IE 9 - `, - }); - - const { files } = await browserBuild(architect, host, target, { watch: true }); - expect(await files['index.html']).toContain( - '' + - '' + - '' + - '', - ); - }, - TEST_TIMEOUT, - ); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts index d3ea014a60fd..7ea22300568e 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts @@ -75,40 +75,6 @@ describe('Browser Builder scripts array', () => { } }); - it('works in watch mode with differential loading', async () => { - const matches: Record = { - 'scripts.js': 'input-script', - 'lazy-script.js': 'lazy-script', - 'renamed-script.js': 'pre-rename-script', - 'renamed-lazy-script.js': 'pre-rename-lazy-script', - 'main-es2017.js': 'input-script', - 'index.html': - '' + - '' + - '' + - '' + - '' + - '', - }; - - host.writeMultipleFiles(scripts); - host.appendToFile('src/main.ts', "\nimport './input-script.js';"); - - // Enable differential loading - host.appendToFile('.browserslistrc', '\nIE 11'); - - // Remove styles so we don't have to account for them in the index.html order check. - const { files } = await browserBuild(architect, host, target, { - styles: [], - scripts: getScriptsOption(), - watch: true, - } as {}); - - for (const [fileName, content] of Object.entries(matches)) { - expect(await files[fileName]).toMatch(content); - } - }); - it('uglifies, uses sourcemaps, and adds hashes', async () => { host.writeMultipleFiles(scripts); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts index eac78d0d56d6..9890857bfd47 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts @@ -140,9 +140,6 @@ describe('Browser Builder styles', () => { '.browserslistrc': 'IE 10', }); - // Set target to ES5 to avoid differential loading and unnecessary testing time - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); - const { files } = await browserBuild(architect, host, target, { aot: false }); expect(await files['main.js']).toContain('-ms-flex: 1;'); @@ -165,9 +162,6 @@ describe('Browser Builder styles', () => { '.browserslistrc': 'IE 10', }); - // Set target to ES5 to avoid differential loading and unnecessary testing time - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); - const { files } = await browserBuild(architect, host, target, { aot: true }); expect(await files['main.js']).toContain('-ms-flex: 1;'); @@ -354,9 +348,6 @@ describe('Browser Builder styles', () => { '.browserslistrc': 'IE 10', }); - // Set to target to ES5 to avoid differential loading and unnecessary testing time - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); - const overrides = { extractCss: true, optimization: false }; const { files } = await browserBuild(architect, host, target, overrides); expect(await files['styles.css']).toContain(tags.stripIndents` @@ -394,9 +385,6 @@ describe('Browser Builder styles', () => { '.browserslistrc': 'IE 10', }); - // Set target to ES5 to avoid differential loading and unnecessary testing time - host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); - const overrides = { extractCss: true, optimization: true, styles: ['src/styles.scss'] }; const { files } = await browserBuild(architect, host, target, overrides); expect(await files['styles.css']).toContain('-ms-grid-columns:100px;'); diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index 7758fc1f3741..e39d63cdaade 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -16,13 +16,12 @@ import { json, tags } from '@angular-devkit/core'; import * as path from 'path'; import { Observable, from } from 'rxjs'; import { concatMap, switchMap } from 'rxjs/operators'; -import * as ts from 'typescript'; import * as url from 'url'; import webpack from 'webpack'; import webpackDevServer from 'webpack-dev-server'; import { Schema as BrowserBuilderSchema, OutputHashing } from '../browser/schema'; import { ExecutionTransformer } from '../transforms'; -import { BuildBrowserFeatures, normalizeOptimization } from '../utils'; +import { normalizeOptimization } from '../utils'; import { findCachePath } from '../utils/cache-path'; import { checkPort } from '../utils/check-port'; import { colors } from '../utils/color'; @@ -303,17 +302,10 @@ export function serveWebpackBrowser( } return from(setup()).pipe( - switchMap(({ browserOptions, webpackConfig, projectRoot, locale }) => { + switchMap(({ browserOptions, webpackConfig, locale }) => { if (browserOptions.index) { - const { scripts = [], styles = [], baseHref, tsConfig } = browserOptions; - const { options: compilerOptions } = readTsconfig(tsConfig, workspaceRoot); - const target = compilerOptions.target || ts.ScriptTarget.ES5; - const buildBrowserFeatures = new BuildBrowserFeatures(projectRoot); - + const { scripts = [], styles = [], baseHref } = browserOptions; const entrypoints = generateEntryPoints({ scripts, styles }); - const moduleEntrypoints = buildBrowserFeatures.isDifferentialLoadingNeeded(target) - ? generateEntryPoints({ scripts: [], styles }) - : []; webpackConfig.plugins = [...(webpackConfig.plugins || [])]; webpackConfig.plugins.push( @@ -322,7 +314,7 @@ export function serveWebpackBrowser( outputPath: getIndexOutputFile(browserOptions.index), baseHref, entrypoints, - moduleEntrypoints, + moduleEntrypoints: [], noModuleEntrypoints: ['polyfills-es5'], deployUrl: browserOptions.deployUrl, sri: browserOptions.subresourceIntegrity, diff --git a/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts index 0326f1481933..7d804e26ef10 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts @@ -17,95 +17,6 @@ describe('Dev Server Builder index', () => { beforeEach(async () => host.initialize().toPromise()); afterEach(async () => host.restore().toPromise()); - it(`adds 'type="module"' when differential loading is needed`, async () => { - host.writeMultipleFiles({ - '.browserslistrc': ` - last 1 chrome version - IE 10 - `, - }); - - const architect = (await createArchitect(host.root())).architect; - const run = await architect.scheduleTarget(targetSpec, { port: 0 }); - const output = (await run.result) as DevServerBuilderOutput; - expect(output.success).toBe(true); - const response = await fetch(output.baseUrl); - expect(await response.text()).toContain( - '' + - '' + - '' + - '', - ); - await run.stop(); - }); - - it(`does not add 'type="module"' to custom scripts when differential loading is needed`, async () => { - host.writeMultipleFiles({ - '.browserslistrc': ` - last 1 chrome version - IE 10 - `, - 'test.js': 'console.log("test");', - }); - - const { workspace } = await workspaces.readWorkspace( - host.root(), - workspaces.createWorkspaceHost(host), - ); - const app = workspace.projects.get('app'); - if (!app) { - fail('Test application "app" not found.'); - - return; - } - const target = app.targets.get('build'); - if (!target) { - fail('Test application "app" target "build" not found.'); - - return; - } - if (!target.options) { - target.options = {}; - } - target.options.scripts = ['test.js']; - await workspaces.writeWorkspace(workspace, workspaces.createWorkspaceHost(host)); - - const architect = (await createArchitect(host.root())).architect; - const run = await architect.scheduleTarget(targetSpec, { port: 0 }); - const output = (await run.result) as DevServerBuilderOutput; - expect(output.success).toBe(true); - const response = await fetch(output.baseUrl); - expect(await response.text()).toContain( - '' + - '' + - '' + - '' + - '', - ); - await run.stop(); - }); - - it(`doesn't 'type="module"' when differential loading is not needed`, async () => { - host.writeMultipleFiles({ - '.browserslistrc': ` - last 1 chrome version - `, - }); - - const architect = (await createArchitect(host.root())).architect; - const run = await architect.scheduleTarget(targetSpec, { port: 0 }); - const output = (await run.result) as DevServerBuilderOutput; - expect(output.success).toBe(true); - const response = await fetch(output.baseUrl); - expect(await response.text()).toContain( - '' + - '' + - '' + - '', - ); - await run.stop(); - }); - it('sets HTML lang attribute with the active locale', async () => { const locale = 'fr'; const { workspace } = await workspaces.readWorkspace( diff --git a/packages/angular_devkit/build_angular/src/utils/action-cache.ts b/packages/angular_devkit/build_angular/src/utils/action-cache.ts deleted file mode 100644 index a9f6b40bdfdf..000000000000 --- a/packages/angular_devkit/build_angular/src/utils/action-cache.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import * as cacache from 'cacache'; -import { createHash } from 'crypto'; -import * as fs from 'fs'; -import { copyFile } from './copy-file'; -import { allowMangle } from './environment-options'; -import { CacheKey, ProcessBundleOptions, ProcessBundleResult } from './process-bundle'; - -const packageVersion = require('../../package.json').version; - -export interface CacheEntry { - path: string; - size: number; - integrity?: string; -} - -export class BundleActionCache { - constructor(private readonly cachePath: string, private readonly integrityAlgorithm?: string) {} - - static copyEntryContent(entry: CacheEntry | string, dest: fs.PathLike): void { - copyFile(typeof entry === 'string' ? entry : entry.path, dest); - - if (process.platform !== 'win32') { - // The cache writes entries as readonly and when using copyFile the permissions will also be copied. - // See: https://github.com/npm/cacache/blob/073fbe1a9f789ba42d9a41de7b8429c93cf61579/lib/util/move-file.js#L36 - fs.chmodSync(dest, 0o644); - } - } - - generateIntegrityValue(content: string): string { - const algorithm = this.integrityAlgorithm || 'sha1'; - const codeHash = createHash(algorithm).update(content).digest('base64'); - - return `${algorithm}-${codeHash}`; - } - - generateBaseCacheKey(content: string): string { - // Create base cache key with elements: - // * package version - different build-angular versions cause different final outputs - // * code length/hash - ensure cached version matches the same input code - const integrity = this.generateIntegrityValue(content); - let baseCacheKey = `${packageVersion}|${content.length}|${integrity}`; - if (!allowMangle) { - baseCacheKey += '|MD'; - } - - return baseCacheKey; - } - - generateCacheKeys(action: ProcessBundleOptions): string[] { - // Postfix added to sourcemap cache keys when vendor, hidden sourcemaps are present - // Allows non-destructive caching of both variants - const sourceMapVendorPostfix = action.sourceMaps && action.vendorSourceMaps ? '|vendor' : ''; - - // sourceMappingURL is added at the very end which causes the code to be the same when sourcemaps are enabled/disabled - // When using hiddenSourceMaps we can omit the postfix since sourceMappingURL will not be added. - // When having sourcemaps a hashed file and non hashed file can have the same content. But the sourceMappingURL will differ. - const sourceMapPostFix = - action.sourceMaps && !action.hiddenSourceMaps ? `|sourcemap|${action.filename}` : ''; - - const baseCacheKey = this.generateBaseCacheKey(action.code); - - // Determine cache entries required based on build settings - const cacheKeys: string[] = []; - - // If optimizing and the original is not ignored, add original as required - if (!action.ignoreOriginal) { - cacheKeys[CacheKey.OriginalCode] = baseCacheKey + sourceMapPostFix + '|orig'; - - // If sourcemaps are enabled, add original sourcemap as required - if (action.sourceMaps) { - cacheKeys[CacheKey.OriginalMap] = baseCacheKey + sourceMapVendorPostfix + '|orig-map'; - } - } - - // If not only optimizing, add downlevel as required - if (!action.optimizeOnly) { - cacheKeys[CacheKey.DownlevelCode] = baseCacheKey + sourceMapPostFix + '|dl'; - - // If sourcemaps are enabled, add downlevel sourcemap as required - if (action.sourceMaps) { - cacheKeys[CacheKey.DownlevelMap] = baseCacheKey + sourceMapVendorPostfix + '|dl-map'; - } - } - - return cacheKeys; - } - - async getCacheEntries(cacheKeys: (string | undefined)[]): Promise<(CacheEntry | null)[] | false> { - // Attempt to get required cache entries - const cacheEntries = []; - for (const key of cacheKeys) { - if (key) { - const entry = await cacache.get.info(this.cachePath, key); - if (!entry) { - return false; - } - cacheEntries.push({ - path: entry.path, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - size: (entry as any).size, - integrity: entry.metadata && entry.metadata.integrity, - }); - } else { - cacheEntries.push(null); - } - } - - return cacheEntries; - } - - async getCachedBundleResult(action: ProcessBundleOptions): Promise { - const entries = action.cacheKeys && (await this.getCacheEntries(action.cacheKeys)); - if (!entries) { - return null; - } - - const result: ProcessBundleResult = { - name: action.name, - integrity: this.generateIntegrityValue(action.code), - }; - - let cacheEntry = entries[CacheKey.OriginalCode]; - if (cacheEntry) { - result.original = { - filename: action.filename, - size: cacheEntry.size, - integrity: cacheEntry.integrity, - }; - - BundleActionCache.copyEntryContent(cacheEntry, result.original.filename); - - cacheEntry = entries[CacheKey.OriginalMap]; - if (cacheEntry) { - result.original.map = { - filename: action.filename + '.map', - size: cacheEntry.size, - }; - - BundleActionCache.copyEntryContent(cacheEntry, result.original.filename + '.map'); - } - } else if (!action.ignoreOriginal) { - // If the original wasn't processed (and therefore not cached), add info - result.original = { - filename: action.filename, - size: Buffer.byteLength(action.code, 'utf8'), - map: - action.map === undefined - ? undefined - : { - filename: action.filename + '.map', - size: Buffer.byteLength(action.map, 'utf8'), - }, - }; - } - - cacheEntry = entries[CacheKey.DownlevelCode]; - if (cacheEntry) { - result.downlevel = { - filename: action.filename.replace(/\-(es20\d{2}|esnext)/, '-es5'), - size: cacheEntry.size, - integrity: cacheEntry.integrity, - }; - - BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename); - - cacheEntry = entries[CacheKey.DownlevelMap]; - if (cacheEntry) { - result.downlevel.map = { - filename: action.filename.replace(/\-(es20\d{2}|esnext)/, '-es5') + '.map', - size: cacheEntry.size, - }; - - BundleActionCache.copyEntryContent(cacheEntry, result.downlevel.filename + '.map'); - } - } - - return result; - } -} diff --git a/packages/angular_devkit/build_angular/src/utils/action-executor.ts b/packages/angular_devkit/build_angular/src/utils/action-executor.ts index d7548192e060..2d7b4fa11115 100644 --- a/packages/angular_devkit/build_angular/src/utils/action-executor.ts +++ b/packages/angular_devkit/build_angular/src/utils/action-executor.ts @@ -7,25 +7,16 @@ */ import Piscina from 'piscina'; -import { BundleActionCache } from './action-cache'; import { maxWorkers } from './environment-options'; import { I18nOptions } from './i18n-options'; -import { InlineOptions, ProcessBundleOptions, ProcessBundleResult } from './process-bundle'; +import { InlineOptions } from './process-bundle'; const workerFile = require.resolve('./process-bundle'); export class BundleActionExecutor { private workerPool?: Piscina; - private cache?: BundleActionCache; - constructor( - private workerOptions: { cachePath?: string; i18n: I18nOptions }, - integrityAlgorithm?: string, - ) { - if (workerOptions.cachePath) { - this.cache = new BundleActionCache(workerOptions.cachePath, integrityAlgorithm); - } - } + constructor(private workerOptions: { i18n: I18nOptions }) {} private ensureWorkerPool(): Piscina { if (this.workerPool) { @@ -34,7 +25,7 @@ export class BundleActionExecutor { this.workerPool = new Piscina({ filename: workerFile, - name: 'process', + name: 'inlineLocales', workerData: this.workerOptions, maxThreads: maxWorkers, }); @@ -42,27 +33,6 @@ export class BundleActionExecutor { return this.workerPool; } - async process(action: ProcessBundleOptions): Promise { - if (this.cache) { - const cacheKeys = this.cache.generateCacheKeys(action); - action.cacheKeys = cacheKeys; - - // Try to get cached data, if it fails fallback to processing - try { - const cachedResult = await this.cache.getCachedBundleResult(action); - if (cachedResult) { - return cachedResult; - } - } catch {} - } - - return this.ensureWorkerPool().run(action, { name: 'process' }); - } - - processAll(actions: Iterable): AsyncIterable { - return BundleActionExecutor.executeAll(actions, (action) => this.process(action)); - } - async inline( action: InlineOptions, ): Promise<{ file: string; diagnostics: { type: string; message: string }[]; count: number }> { diff --git a/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts b/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts index 3be2a093fb05..958dca8e47b3 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts @@ -17,16 +17,6 @@ export class BuildBrowserFeatures { this.supportedBrowsers = browserslist(undefined, { path: this.projectRoot }); } - /** - * True, when one or more browsers requires ES5 - * support and the script target is ES2015 or greater. - */ - isDifferentialLoadingNeeded(scriptTarget: ts.ScriptTarget): boolean { - const es6TargetOrLater = scriptTarget > ts.ScriptTarget.ES5; - - return es6TargetOrLater && this.isEs5SupportNeeded(); - } - /** * True, when one or more browsers requires ES5 support */ diff --git a/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts b/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts index 69220e4dba24..1f5ffd62a1c6 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts @@ -23,44 +23,6 @@ describe('BuildBrowserFeatures', () => { afterEach(async () => host.restore().toPromise()); - describe('isDifferentialLoadingNeeded', () => { - it('should be true for IE 9-11 and ES2017', () => { - host.writeMultipleFiles({ - '.browserslistrc': 'IE 9-11', - }); - - const buildBrowserFeatures = new BuildBrowserFeatures(workspaceRootSysPath); - expect(buildBrowserFeatures.isDifferentialLoadingNeeded(ScriptTarget.ES2017)).toBe(true); - }); - - it('should be false for Chrome and ES2017', () => { - host.writeMultipleFiles({ - '.browserslistrc': 'last 1 chrome version', - }); - - const buildBrowserFeatures = new BuildBrowserFeatures(workspaceRootSysPath); - expect(buildBrowserFeatures.isDifferentialLoadingNeeded(ScriptTarget.ES2017)).toBe(false); - }); - - it('detects no need for differential loading for target is ES5', () => { - host.writeMultipleFiles({ - '.browserslistrc': 'last 1 chrome version', - }); - - const buildBrowserFeatures = new BuildBrowserFeatures(workspaceRootSysPath); - expect(buildBrowserFeatures.isDifferentialLoadingNeeded(ScriptTarget.ES5)).toBe(false); - }); - - it('should be false for Safari 10.1 when target is ES2017', () => { - host.writeMultipleFiles({ - '.browserslistrc': 'Safari 10.1', - }); - - const buildBrowserFeatures = new BuildBrowserFeatures(workspaceRootSysPath); - expect(buildBrowserFeatures.isDifferentialLoadingNeeded(ScriptTarget.ES2017)).toBe(false); - }); - }); - describe('isFeatureSupported', () => { it('should be true for es6-module and Safari 10.1', () => { host.writeMultipleFiles({ diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts index 8ebd51c7e9cb..52b3265ed429 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts @@ -70,8 +70,6 @@ export interface BuildOptions { inlineStyleLanguage?: InlineStyleLanguage; allowedCommonJsDependencies?: string[]; - - differentialLoadingNeeded?: boolean; } export interface WebpackTestOptions extends BuildOptions { diff --git a/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts index a1021043f37d..6b15ed64a566 100644 --- a/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { basename } from 'path'; import { StatsAsset, StatsChunk, StatsCompilation } from 'webpack'; import { Budget, Type } from '../browser/schema'; -import { ProcessBundleFile, ProcessBundleResult } from '../utils/process-bundle'; import { formatSize } from '../webpack/utils/stats'; interface Size { @@ -33,11 +31,6 @@ export enum ThresholdSeverity { Error = 'error', } -enum DifferentialBuildType { - ORIGINAL = 'original', - DOWNLEVEL = 'downlevel', -} - export function* calculateThresholds(budget: Budget): IterableIterator { if (budget.maximumWarning) { yield { @@ -103,11 +96,7 @@ export function* calculateThresholds(budget: Budget): IterableIterator; type CalculatorTypes = { - new ( - budget: Budget, - chunks: StatsChunk[], - assets: StatsAsset[], - processResults: ProcessBundleResult[], - ): Calculator; + new (budget: Budget, chunks: StatsChunk[], assets: StatsAsset[]): Calculator; }; const calculatorMap: Record = { all: AllCalculator, @@ -143,7 +127,7 @@ function calculateSizes( throw new Error('Webpack stats output did not include asset information.'); } - const calculator = new ctor(budget, chunks, assets, processResults); + const calculator = new ctor(budget, chunks, assets); return calculator.calculate(); } @@ -153,54 +137,31 @@ abstract class Calculator { protected budget: Budget, protected chunks: StatsChunk[], protected assets: StatsAsset[], - protected processResults: ProcessBundleResult[], ) {} abstract calculate(): Size[]; /** Calculates the size of the given chunk for the provided build type. */ - protected calculateChunkSize(chunk: StatsChunk, buildType: DifferentialBuildType): number { - // Look for a process result containing different builds for this chunk. - const processResult = this.processResults.find( - (processResult) => processResult.name === chunk.id?.toString(), - ); - - if (processResult) { - // Found a differential build, use the correct size information. - const processResultFile = getDifferentialBuildResult(processResult, buildType); + protected calculateChunkSize(chunk: StatsChunk): number { + // No differential builds, get the chunk size by summing its assets. + if (!chunk.files) { + return 0; + } - return (processResultFile && processResultFile.size) || 0; - } else { - // No differential builds, get the chunk size by summing its assets. - if (!chunk.files) { - return 0; - } + return chunk.files + .filter((file) => !file.endsWith('.map')) + .map((file) => { + const asset = this.assets.find((asset) => asset.name === file); + if (!asset) { + throw new Error(`Could not find asset for file: ${file}`); + } - return chunk.files - .filter((file) => !file.endsWith('.map')) - .map((file) => { - const asset = this.assets.find((asset) => asset.name === file); - if (!asset) { - throw new Error(`Could not find asset for file: ${file}`); - } - - return asset.size; - }) - .reduce((l, r) => l + r, 0); - } + return asset.size; + }) + .reduce((l, r) => l + r, 0); } protected getAssetSize(asset: StatsAsset): number { - if (asset.name.endsWith('.js')) { - const processResult = this.processResults.find( - (processResult) => - processResult.original && basename(processResult.original.filename) === asset.name, - ); - if (processResult?.original) { - return processResult.original.size; - } - } - return asset.size; } } @@ -215,26 +176,12 @@ class BundleCalculator extends Calculator { return []; } - const buildTypeLabels = getBuildTypeLabels(this.processResults); - - // The chunk may or may not have differential builds. Compute the size for - // each then check afterwards if they are all the same. - const buildSizes = Object.values(DifferentialBuildType).map((buildType) => { - const size = this.chunks - .filter((chunk) => chunk?.names?.includes(budgetName)) - .map((chunk) => this.calculateChunkSize(chunk, buildType)) - .reduce((l, r) => l + r, 0); - - return { size, label: `bundle ${this.budget.name}-${buildTypeLabels[buildType]}` }; - }); - - // If this bundle was not actually generated by a differential build, then - // merge the results into a single value. - if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) { - return mergeDifferentialBuildSizes(buildSizes, budgetName); - } else { - return buildSizes; - } + const size = this.chunks + .filter((chunk) => chunk?.names?.includes(budgetName)) + .map((chunk) => this.calculateChunkSize(chunk)) + .reduce((l, r) => l + r, 0); + + return [{ size, label: `bundle ${this.budget.name}` }]; } } @@ -243,24 +190,15 @@ class BundleCalculator extends Calculator { */ class InitialCalculator extends Calculator { calculate() { - const buildTypeLabels = getBuildTypeLabels(this.processResults); - const buildSizes = Object.values(DifferentialBuildType).map((buildType) => { - return { - label: `bundle initial-${buildTypeLabels[buildType]}`, + return [ + { + label: `bundle initial`, size: this.chunks .filter((chunk) => chunk.initial) - .map((chunk) => this.calculateChunkSize(chunk, buildType)) + .map((chunk) => this.calculateChunkSize(chunk)) .reduce((l, r) => l + r, 0), - }; - }); - - // If this bundle was not actually generated by a differential build, then - // merge the results into a single value. - if (allEquivalent(buildSizes.map((buildSize) => buildSize.size))) { - return mergeDifferentialBuildSizes(buildSizes, 'initial'); - } else { - return buildSizes; - } + }, + ]; } } @@ -357,13 +295,12 @@ function calculateBytes(input: string, baseline?: string, factor: 1 | -1 = 1): n export function* checkBudgets( budgets: Budget[], webpackStats: StatsCompilation, - processResults: ProcessBundleResult[], ): IterableIterator<{ severity: ThresholdSeverity; message: string }> { // Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker`. const computableBudgets = budgets.filter((budget) => budget.type !== Type.AnyComponentStyle); for (const budget of computableBudgets) { - const sizes = calculateSizes(budget, webpackStats, processResults); + const sizes = calculateSizes(budget, webpackStats); for (const { size, label } of sizes) { yield* checkThresholds(calculateThresholds(budget), size, label); } @@ -411,60 +348,3 @@ export function* checkThresholds( } } } - -/** Returns the {@link ProcessBundleFile} for the given {@link DifferentialBuildType}. */ -function getDifferentialBuildResult( - processResult: ProcessBundleResult, - buildType: DifferentialBuildType, -): ProcessBundleFile | null { - switch (buildType) { - case DifferentialBuildType.ORIGINAL: - return processResult.original || null; - case DifferentialBuildType.DOWNLEVEL: - return processResult.downlevel || null; - } -} - -/** - * Merges the given differential builds into a single, non-differential value. - * - * Preconditions: All the sizes should be equivalent, or else they represent - * differential builds. - */ -function mergeDifferentialBuildSizes(buildSizes: Size[], mergeLabel: string): Size[] { - if (buildSizes.length === 0) { - return []; - } - - // Only one size. - return [ - { - label: mergeLabel, - size: buildSizes[0].size, - }, - ]; -} - -/** Returns whether or not all items in the list are equivalent to each other. */ -function allEquivalent(items: Iterable): boolean { - return new Set(items).size < 2; -} - -function getBuildTypeLabels( - processResults: ProcessBundleResult[], -): Record { - const fileNameSuffixRegExp = /\-(es20\d{2}|esnext)\./; - const originalFileName = processResults.find( - ({ original }) => original?.filename && fileNameSuffixRegExp.test(original.filename), - )?.original?.filename; - - let originalSuffix: string | undefined; - if (originalFileName) { - originalSuffix = fileNameSuffixRegExp.exec(originalFileName)?.[1]; - } - - return { - [DifferentialBuildType.DOWNLEVEL]: 'es5', - [DifferentialBuildType.ORIGINAL]: originalSuffix || 'es2015', - }; -} diff --git a/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts b/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts index 47785cd84eb9..bd826a8874fc 100644 --- a/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts +++ b/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts @@ -9,7 +9,6 @@ import { StatsCompilation } from 'webpack'; import { Budget, Type } from '../browser/schema'; import { ThresholdSeverity, checkBudgets } from './bundle-calculator'; -import { ProcessBundleResult } from './process-bundle'; const KB = 1024; @@ -22,7 +21,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [], assets: [ { @@ -34,9 +33,9 @@ describe('bundle-calculator', () => { size: 0.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -52,7 +51,7 @@ describe('bundle-calculator', () => { minimumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [], assets: [ { @@ -64,9 +63,9 @@ describe('bundle-calculator', () => { size: 0.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -83,7 +82,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -101,9 +100,9 @@ describe('bundle-calculator', () => { size: 0.75 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -112,92 +111,6 @@ describe('bundle-calculator', () => { }); }); - it('yields exceeded differential bundle budgets', () => { - const budgets: Budget[] = [ - { - type: Type.Bundle, - name: 'foo', - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - names: ['foo'], - files: ['foo.js', 'bar.js'], - }, - ], - assets: [], - } as unknown) as StatsCompilation; - - const processResults: ProcessBundleResult[] = [ - { - name: '0', - original: { - filename: 'foo-es2015.js', - size: 1.25 * KB, - }, - downlevel: { - filename: 'foo-es5.js', - size: 1.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - expect(failures.length).toBe(2); - expect(failures).toContain({ - severity: ThresholdSeverity.Error, - message: jasmine.stringMatching('bundle foo-es2015 exceeded maximum budget.'), - }); - expect(failures).toContain({ - severity: ThresholdSeverity.Error, - message: jasmine.stringMatching('bundle foo-es5 exceeded maximum budget.'), - }); - }); - - it('does *not* yield a combined differential bundle budget', () => { - const budgets: Budget[] = [ - { - type: Type.Bundle, - name: 'foo', - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - names: ['foo'], - files: ['foo.js', 'bar.js'], - }, - ], - assets: [], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: 'foo-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: 'foo-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); - it('yields exceeded initial budget', () => { const budgets: Budget[] = [ { @@ -205,7 +118,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -224,9 +137,9 @@ describe('bundle-calculator', () => { size: 0.75 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -235,92 +148,6 @@ describe('bundle-calculator', () => { }); }); - it('yields exceeded differential initial budget', () => { - const budgets: Budget[] = [ - { - type: Type.Initial, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js', 'bar.js'], - }, - ], - assets: [], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: 'initial-es2017.js', - size: 1.25 * KB, - }, - downlevel: { - filename: 'initial-es5.js', - size: 1.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - expect(failures.length).toBe(2); - expect(failures).toContain({ - severity: ThresholdSeverity.Error, - message: jasmine.stringMatching('bundle initial-es2017 exceeded maximum budget.'), - }); - expect(failures).toContain({ - severity: ThresholdSeverity.Error, - message: jasmine.stringMatching('bundle initial-es5 exceeded maximum budget.'), - }); - }); - - it('does *not* yield a combined differential initial budget', () => { - const budgets: Budget[] = [ - { - type: Type.Initial, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js', 'bar.js'], - }, - ], - assets: [], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: 'initial-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: 'initial-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); - it('yields exceeded total scripts budget', () => { const budgets: Budget[] = [ { @@ -328,7 +155,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -351,9 +178,9 @@ describe('bundle-calculator', () => { size: 1.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -369,7 +196,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -388,9 +215,9 @@ describe('bundle-calculator', () => { size: 0.75 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -406,7 +233,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -425,9 +252,9 @@ describe('bundle-calculator', () => { size: 0.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(0); }); @@ -439,7 +266,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -458,9 +285,9 @@ describe('bundle-calculator', () => { size: 0.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -476,7 +303,7 @@ describe('bundle-calculator', () => { maximumError: '1kb', }, ]; - const stats = ({ + const stats = { chunks: [ { id: 0, @@ -495,9 +322,9 @@ describe('bundle-calculator', () => { size: 0.5 * KB, }, ], - } as unknown) as StatsCompilation; + } as unknown as StatsCompilation; - const failures = Array.from(checkBudgets(budgets, stats, [] /* processResults */)); + const failures = Array.from(checkBudgets(budgets, stats)); expect(failures.length).toBe(1); expect(failures).toContain({ @@ -505,185 +332,5 @@ describe('bundle-calculator', () => { message: jasmine.stringMatching('foo.ext exceeded maximum budget.'), }); }); - - it('does *not* yield a combined differential bundle budget for any script', () => { - const budgets: Budget[] = [ - { - type: Type.AnyScript, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js'], - }, - ], - assets: [ - { - name: 'main-es2015.js', - size: 1.25 * KB, - }, - ], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: '/home/main-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: '/home/main-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); - - it('does *not* yield a combined differential bundle budget for all script', () => { - const budgets: Budget[] = [ - { - type: Type.AllScript, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js'], - }, - ], - assets: [ - { - name: 'main-es2015.js', - size: 1.25 * KB, - }, - ], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: '/home/main-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: '/home/main-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); - - it('does *not* yield a combined differential bundle budget for total budget', () => { - const budgets: Budget[] = [ - { - type: Type.All, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js'], - }, - ], - assets: [ - { - name: 'main-es2015.js', - size: 1.25 * KB, - }, - ], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: '/home/main-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: '/home/main-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); - - it('does *not* yield a combined differential bundle budget for individual file budget', () => { - const budgets: Budget[] = [ - { - type: Type.Any, - maximumError: '1kb', - }, - ]; - const stats = ({ - chunks: [ - { - id: 0, - initial: true, - names: ['foo'], - files: ['foo.js'], - }, - ], - assets: [ - { - name: 'main-es2015.js', - size: 1.25 * KB, - }, - ], - } as unknown) as StatsCompilation; - const processResults: ProcessBundleResult[] = [ - { - name: '0', - // Individual builds are under budget, but combined they are over. - original: { - filename: '/home/main-es2015.js', - size: 0.5 * KB, - }, - downlevel: { - filename: '/home/main-es5.js', - size: 0.75 * KB, - }, - }, - ]; - - const failures = Array.from(checkBudgets(budgets, stats, processResults)); - - // Because individual builds are under budget, they are acceptable. Should - // **not** yield a combined build which is over budget. - expect(failures.length).toBe(0); - }); }); }); diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 04379022fce8..5b2dc4ae8911 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -10,7 +10,6 @@ import remapping from '@ampproject/remapping'; import { NodePath, ParseResult, - PluginObj, parseSync, transformAsync, transformFromAstSync, @@ -18,13 +17,10 @@ import { types, } from '@babel/core'; import templateBuilder from '@babel/template'; -import * as cacache from 'cacache'; -import { createHash } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; -import { minify } from 'terser'; import { workerData } from 'worker_threads'; -import { allowMangle, allowMinify, shouldBeautify } from './environment-options'; +import { allowMinify, shouldBeautify } from './environment-options'; import { I18nOptions } from './i18n-options'; type LocalizeUtilities = typeof import('@angular/localize/src/tools/src/source_file_utils'); @@ -36,369 +32,7 @@ type SourceMapInput = Exclude[0], unknown[]>; // Webpack is only imported if needed during the processing let webpackSources: typeof import('webpack').sources | undefined; -// If code size is larger than 500KB, consider lower fidelity but faster sourcemap merge -const FAST_SOURCEMAP_THRESHOLD = 500 * 1024; - -export interface ProcessBundleOptions { - filename: string; - code: string; - map?: string; - name: string; - sourceMaps?: boolean; - hiddenSourceMaps?: boolean; - vendorSourceMaps?: boolean; - runtime?: boolean; - optimize?: boolean; - optimizeOnly?: boolean; - ignoreOriginal?: boolean; - cacheKeys?: (string | undefined)[]; - integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512'; - runtimeData?: ProcessBundleResult[]; - replacements?: [string, string][]; - supportedBrowsers?: string[] | Record; -} - -export interface ProcessBundleResult { - name: string; - integrity?: string; - original?: ProcessBundleFile; - downlevel?: ProcessBundleFile; -} - -export interface ProcessBundleFile { - filename: string; - size: number; - integrity?: string; - map?: { - filename: string; - size: number; - }; -} - -export const enum CacheKey { - OriginalCode = 0, - OriginalMap = 1, - DownlevelCode = 2, - DownlevelMap = 3, -} - -const { cachePath, i18n } = (workerData || {}) as { cachePath?: string; i18n?: I18nOptions }; - -async function cachePut( - content: string, - key: string | undefined, - integrity?: string, -): Promise { - if (cachePath && key) { - await cacache.put(cachePath, key, content, { - metadata: { integrity }, - }); - } -} - -export async function process(options: ProcessBundleOptions): Promise { - if (!options.cacheKeys) { - options.cacheKeys = []; - } - - const result: ProcessBundleResult = { name: options.name }; - if (options.integrityAlgorithm) { - // Store unmodified code integrity value -- used for SRI value replacement - result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code); - } - - // Runtime chunk requires specialized handling - if (options.runtime) { - return { ...result, ...(await processRuntime(options)) }; - } - - const basePath = path.dirname(options.filename); - const filename = path.basename(options.filename); - const downlevelFilename = filename.replace(/\-(es20\d{2}|esnext)/, '-es5'); - const downlevel = !options.optimizeOnly; - const sourceCode = options.code; - - if (downlevel) { - const { supportedBrowsers: targets = [] } = options; - - // todo: revisit this in version 10, when we update our defaults browserslist - // Without this workaround bundles will not be downlevelled because Babel doesn't know handle to 'op_mini all' - // See: https://github.com/babel/babel/issues/11155 - if (Array.isArray(targets) && targets.includes('op_mini all')) { - targets.push('ie_mob 11'); - } else if ('op_mini' in targets) { - targets['ie_mob'] = '11'; - } - - // Downlevel the bundle - const transformResult = await transformAsync(sourceCode, { - filename, - // using false ensures that babel will NOT search and process sourcemap comments (large memory usage) - // The types do not include the false option even though it is valid - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inputSourceMap: false as any, - babelrc: false, - configFile: false, - presets: [ - [ - require.resolve('@babel/preset-env'), - { - // browserslist-compatible query or object of minimum environment versions to support - targets, - // modules aren't needed since the bundles use webpack's custom module loading - modules: false, - // 'transform-typeof-symbol' generates slower code - exclude: ['transform-typeof-symbol'], - }, - ], - ], - plugins: [ - createIifeWrapperPlugin(), - ...(options.replacements ? [createReplacePlugin(options.replacements)] : []), - ], - minified: allowMinify && !!options.optimize, - compact: !shouldBeautify && !!options.optimize, - sourceMaps: !!options.map, - }); - - if (!transformResult || !transformResult.code) { - throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); - } - - result.downlevel = await processBundle({ - ...options, - code: transformResult.code, - downlevelMap: (transformResult.map as SourceMapInput) ?? undefined, - filename: path.join(basePath, downlevelFilename), - isOriginal: false, - }); - } - - if (!result.original && !options.ignoreOriginal) { - result.original = await processBundle({ - ...options, - isOriginal: true, - }); - } - - return result; -} - -async function processBundle( - options: ProcessBundleOptions & { - isOriginal: boolean; - downlevelMap?: SourceMapInput; - }, -): Promise { - const { - optimize, - isOriginal, - code, - map, - downlevelMap, - filename: filepath, - hiddenSourceMaps, - cacheKeys = [], - integrityAlgorithm, - } = options; - - const filename = path.basename(filepath); - let resultCode = code; - - let optimizeResult; - if (optimize) { - optimizeResult = await terserMangle(code, { - filename, - sourcemap: !!map, - compress: !isOriginal, // We only compress bundles which are downlevelled. - ecma: isOriginal ? 2015 : 5, - }); - resultCode = optimizeResult.code; - } - - let mapContent: string | undefined; - if (map) { - if (!hiddenSourceMaps) { - resultCode += `\n//# sourceMappingURL=${filename}.map`; - } - - const partialSourcemaps: SourceMapInput[] = []; - if (optimizeResult && optimizeResult.map) { - partialSourcemaps.push(optimizeResult.map); - } - if (downlevelMap) { - partialSourcemaps.push(downlevelMap); - } - - if (partialSourcemaps.length > 0) { - partialSourcemaps.push(map); - const fullSourcemap = remapping(partialSourcemaps, () => null); - mapContent = JSON.stringify(fullSourcemap); - } else { - mapContent = map; - } - - await cachePut( - mapContent, - cacheKeys[isOriginal ? CacheKey.OriginalMap : CacheKey.DownlevelMap], - ); - fs.writeFileSync(filepath + '.map', mapContent); - } - - const fileResult = createFileEntry(filepath, resultCode, mapContent, integrityAlgorithm); - - await cachePut( - resultCode, - cacheKeys[isOriginal ? CacheKey.OriginalCode : CacheKey.DownlevelCode], - fileResult.integrity, - ); - fs.writeFileSync(filepath, resultCode); - - return fileResult; -} - -async function terserMangle( - code: string, - options: { filename?: string; sourcemap?: boolean; compress?: boolean; ecma?: 5 | 2015 } = {}, -) { - // Note: Investigate converting the AST instead of re-parsing - // estree -> terser is already supported; need babel -> estree/terser - - // Mangle downlevel code - const minifyOutput = await minify(options.filename ? { [options.filename]: code } : code, { - compress: allowMinify && !!options.compress, - ecma: options.ecma || 5, - mangle: allowMangle, - safari10: true, - format: { - ascii_only: true, - webkit: true, - beautify: shouldBeautify, - wrap_func_args: false, - }, - sourceMap: - !!options.sourcemap && - ({ - asObject: true, - // typings don't include asObject option - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any), - }); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return { code: minifyOutput.code!, map: minifyOutput.map as SourceMapInput | undefined }; -} - -function createFileEntry( - filename: string, - code: string, - map: string | undefined, - integrityAlgorithm?: string, -): ProcessBundleFile { - return { - filename: filename, - size: Buffer.byteLength(code), - integrity: integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code), - map: !map - ? undefined - : { - filename: filename + '.map', - size: Buffer.byteLength(map), - }, - }; -} - -function generateIntegrityValue(hashAlgorithm: string, code: string) { - return hashAlgorithm + '-' + createHash(hashAlgorithm).update(code).digest('base64'); -} - -// The webpack runtime chunk is already ES5. -// However, two variants are still needed due to lazy routing and SRI differences -// NOTE: This should eventually be a babel plugin -async function processRuntime( - options: ProcessBundleOptions, -): Promise> { - let originalCode = options.code; - let downlevelCode = options.code; - - // Replace integrity hashes with updated values - if (options.integrityAlgorithm && options.runtimeData) { - for (const data of options.runtimeData) { - if (!data.integrity) { - continue; - } - - if (data.original && data.original.integrity) { - originalCode = originalCode.replace(data.integrity, data.original.integrity); - } - if (data.downlevel && data.downlevel.integrity) { - downlevelCode = downlevelCode.replace(data.integrity, data.downlevel.integrity); - } - } - } - - // Adjust lazy loaded scripts to point to the proper variant - // Extra spacing is intentional to align source line positions - downlevelCode = downlevelCode.replace(/"\-(es20\d{2}|esnext)\./, ' "-es5.'); - - return { - original: await processBundle({ - ...options, - code: originalCode, - isOriginal: true, - }), - downlevel: await processBundle({ - ...options, - code: downlevelCode, - filename: options.filename.replace(/\-(es20\d{2}|esnext)/, '-es5'), - isOriginal: false, - }), - }; -} - -function createReplacePlugin(replacements: [string, string][]): PluginObj { - return { - visitor: { - StringLiteral(path: NodePath) { - for (const replacement of replacements) { - if (path.node.value === replacement[0]) { - path.node.value = replacement[1]; - } - } - }, - }, - }; -} - -function createIifeWrapperPlugin(): PluginObj { - return { - visitor: { - Program: { - exit(path: NodePath) { - // Save existing body and directives - const { body, directives } = path.node; - - // Clear out body and directives for wrapper - path.node.body = []; - path.node.directives = []; - - // Create the wrapper - "(function() { ... })();" - const wrapper = types.expressionStatement( - types.callExpression( - types.parenthesizedExpression( - types.functionExpression(undefined, [], types.blockStatement(body, directives)), - ), - [], - ), - ); - - // Insert the wrapper - path.pushContainer('body', wrapper); - }, - }, - }, - }; -} +const { i18n } = (workerData || {}) as { i18n?: I18nOptions }; const USE_LOCALIZE_PLUGINS = false; diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts index c757c70459d5..2f292b323751 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts @@ -40,12 +40,7 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati if (scriptsSourceMap || stylesSourceMap) { extraPlugins.push( - getSourceMapDevTool( - scriptsSourceMap, - stylesSourceMap, - buildOptions.differentialLoadingNeeded && !buildOptions.watch ? true : hiddenSourceMap, - false, - ), + getSourceMapDevTool(scriptsSourceMap, stylesSourceMap, hiddenSourceMap, false), ); } diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 313e1538981f..1894f37a5bd2 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -70,11 +70,6 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { const hashFormat = getOutputHashFormat(buildOptions.outputHashing || 'none'); const buildBrowserFeatures = new BuildBrowserFeatures(projectRoot); - const targetInFileName = getEsVersionForFileName( - tsConfig.options.target, - buildOptions.differentialLoadingNeeded, - ); - if (buildOptions.progress) { const spinner = new Spinner(); spinner.start(`Generating ${platform} application bundles (phase: setup)...`); @@ -111,23 +106,17 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { entryPoints['main'] = [mainPath]; } - const differentialLoadingMode = buildOptions.differentialLoadingNeeded && !buildOptions.watch; if (platform !== 'server') { - if (differentialLoadingMode || tsConfig.options.target === ScriptTarget.ES5) { - if (buildBrowserFeatures.isEs5SupportNeeded()) { - const polyfillsChunkName = 'polyfills-es5'; - entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')]; - - if (!buildOptions.aot) { - if (differentialLoadingMode) { - entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'jit-polyfills.js')); - } - entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js')); - } - // If not performing a full differential build the polyfills need to be added to ES5 bundle - if (buildOptions.polyfills) { - entryPoints[polyfillsChunkName].push(path.resolve(root, buildOptions.polyfills)); - } + if (buildBrowserFeatures.isEs5SupportNeeded()) { + const polyfillsChunkName = 'polyfills-es5'; + entryPoints[polyfillsChunkName] = [path.join(__dirname, '..', 'es5-polyfills.js')]; + + if (!buildOptions.aot) { + entryPoints[polyfillsChunkName].push(path.join(__dirname, '..', 'es5-jit-polyfills.js')); + } + + if (buildOptions.polyfills) { + entryPoints[polyfillsChunkName].push(path.resolve(root, buildOptions.polyfills)); } } @@ -409,10 +398,10 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { if (chunk?.name === 'polyfills-es5') { return `polyfills-es5${hashFormat.chunk}.js`; } else { - return `[name]${targetInFileName}${hashFormat.chunk}.js`; + return `[name]${hashFormat.chunk}.js`; } }, - chunkFilename: `[name]${targetInFileName}${hashFormat.chunk}.js`, + chunkFilename: `[name]${hashFormat.chunk}.js`, }, watch: buildOptions.watch, watchOptions: getWatchOptions(buildOptions.poll), diff --git a/tests/legacy-cli/e2e/tests/basic/build.ts b/tests/legacy-cli/e2e/tests/basic/build.ts index 891ad059516a..24d0ebf486d9 100644 --- a/tests/legacy-cli/e2e/tests/basic/build.ts +++ b/tests/legacy-cli/e2e/tests/basic/build.ts @@ -1,4 +1,4 @@ -import { appendToFile, expectFileToMatch } from '../../utils/fs'; +import { expectFileToMatch } from '../../utils/fs'; import { ng } from '../../utils/process'; export default async function () { @@ -11,24 +11,16 @@ export default async function () { await ng('build', '--configuration=development', 'test-project', '--no-progress'); await ng('build', '--configuration=development', '--no-progress', 'test-project'); - // Enable Differential loading to run both size checks - await appendToFile('.browserslistrc', 'IE 11'); // Production build const { stderr: stderrProgress, stdout } = await ng('build', '--progress'); - await expectFileToMatch('dist/test-project/index.html', /main-es5\.[a-zA-Z0-9]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /main-es2017\.[a-zA-Z0-9]{20}\.js/); + await expectFileToMatch('dist/test-project/index.html', /main\.[a-zA-Z0-9]{20}\.js/); - if (!stdout.includes('Initial ES5 Total')) { - throw new Error(`Expected stdout not to contain 'Initial ES5 Total' but it did.\n${stdout}`); - } - - if (!stdout.includes('Initial ES2017 Total')) { - throw new Error(`Expected stdout not to contain 'Initial ES2017 Total' but it did.\n${stdout}`); + if (!stdout.includes('Initial Total')) { + throw new Error(`Expected stdout to contain 'Initial Total' but it did not.\n${stdout}`); } const logs: string[] = [ 'Browser application bundle generation complete', - 'ES5 bundle generation complete', 'Copying assets complete', 'Index html generation complete', ]; diff --git a/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts b/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts index 433511947929..d079ec2e5197 100644 --- a/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts +++ b/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts @@ -8,10 +8,10 @@ import { ng } from '../../utils/process'; -export default async function() { +export default async function () { const { stderr, stdout } = await ng('build'); if (stdout.includes('as esm5') || stderr.includes('as esm5')) { - throw new Error('ngcc should not process ES5 during differential loading builds.'); + throw new Error('ngcc should not process ES5 during builds.'); } } diff --git a/tests/legacy-cli/e2e/tests/build/differential-cache.ts b/tests/legacy-cli/e2e/tests/build/differential-cache.ts deleted file mode 100644 index 9e93212b0e4b..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-cache.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import { rimraf, appendToFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -function generateFileHashMap(): Map { - const hashes = new Map(); - - fs.readdirSync('./dist/test-project').forEach((name) => { - const data = fs.readFileSync('./dist/test-project/' + name); - const hash = crypto.createHash('sha1').update(data).digest('hex'); - - hashes.set(name, hash); - }); - - return hashes; -} - -function validateHashes( - oldHashes: Map, - newHashes: Map, - shouldChange: Array, -): void { - oldHashes.forEach((hash, name) => { - if (hash === newHashes.get(name)) { - if (shouldChange.includes(name)) { - throw new Error(`"${name}" did not change hash (${hash})...`); - } - } else if (!shouldChange.includes(name)) { - throw new Error(`"${name}" changed hash (${hash})...`); - } - }); -} - -export default async function () { - // Skip on CI due to large variability of performance - if (process.env['CI']) { - return; - } - - let oldHashes: Map; - let newHashes: Map; - - // Enable Differential loading to run both size checks - await appendToFile('.browserslistrc', 'IE 11'); - - // Remove the cache so that an initial build and build with cache can be tested - await rimraf('./node_modules/.cache'); - - let start = Date.now(); - await ng('build', '--configuration=development'); - let initial = Date.now() - start; - oldHashes = generateFileHashMap(); - - start = Date.now(); - await ng('build', '--configuration=development'); - let cached = Date.now() - start; - newHashes = generateFileHashMap(); - - validateHashes(oldHashes, newHashes, []); - - if (cached > initial * 0.7) { - throw new Error( - `Cached build time [${cached}] should not be greater than 70% of initial build time [${initial}].`, - ); - } - - // Remove the cache so that an initial build and build with cache can be tested - await rimraf('./node_modules/.cache'); - - start = Date.now(); - await ng('build'); - initial = Date.now() - start; - oldHashes = generateFileHashMap(); - - start = Date.now(); - await ng('build'); - cached = Date.now() - start; - newHashes = generateFileHashMap(); - - if (cached > initial * 0.7) { - throw new Error( - `Cached build time [${cached}] should not be greater than 70% of initial build time [${initial}].`, - ); - } - - validateHashes(oldHashes, newHashes, []); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts b/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts deleted file mode 100644 index 0456079a0c18..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createHash } from 'crypto'; -import { - appendToFile, - expectFileToMatch, - prependToFile, - readFile, - replaceInFile, - writeFile, -} from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - // Enable Differential loading - await appendToFile('.browserslistrc', 'IE 11'); - - const appRoutingModulePath = 'src/app/app-routing.module.ts'; - - // Add app routing. - // This is done automatically on a new app with --routing. - await writeFile( - appRoutingModulePath, - ` - import { NgModule } from '@angular/core'; - import { Routes, RouterModule } from '@angular/router'; - - const routes: Routes = []; - - @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] - }) - export class AppRoutingModule { } - `, - ); - await prependToFile( - 'src/app/app.module.ts', - `import { AppRoutingModule } from './app-routing.module';`, - ); - await replaceInFile('src/app/app.module.ts', `imports: [`, `imports: [ AppRoutingModule,`); - await appendToFile('src/app/app.component.html', ''); - - await ng('generate', 'module', 'lazy', '--module=app.module', '--route', 'lazy'); - - await ng('build', '--subresource-integrity', '--output-hashing=none', '--output-path=dist/first'); - - // Second build used to ensure cached files use correct integrity values - await ng( - 'build', - '--subresource-integrity', - '--output-hashing=none', - '--output-path=dist/second', - ); - - const chunkId = '86'; - const codeHashES5 = createHash('sha384') - .update(await readFile(`dist/first/${chunkId}-es5.js`)) - .digest('base64'); - const codeHashes2017 = createHash('sha384') - .update(await readFile(`dist/first/${chunkId}-es2017.js`)) - .digest('base64'); - - await expectFileToMatch('dist/first/runtime-es5.js', 'sha384-' + codeHashES5); - await expectFileToMatch('dist/first/runtime-es2017.js', 'sha384-' + codeHashes2017); - - await expectFileToMatch('dist/second/runtime-es5.js', 'sha384-' + codeHashES5); - await expectFileToMatch('dist/second/runtime-es2017.js', 'sha384-' + codeHashes2017); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts b/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts deleted file mode 100644 index 9cf0d95cb552..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { appendToFile, expectFileToExist } from '../../utils/fs'; -import { execAndWaitForOutputToMatch } from '../../utils/process'; - -export default async function () { - // Enable Differential loading to run both size checks - await appendToFile('.browserslistrc', 'IE 11'); - - await execAndWaitForOutputToMatch( - 'ng', - ['build', '--watch', '--configuration=development'], - /Initial Total/i, - ); - await expectFileToExist('dist/test-project/runtime-es2017.js'); - await expectFileToExist('dist/test-project/main-es2017.js'); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading.ts b/tests/legacy-cli/e2e/tests/build/differential-loading.ts deleted file mode 100644 index 5800a6f1cebc..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - // Enable Differential loading to run both size checks - await appendToFile('.browserslistrc', 'IE 11'); - - await writeMultipleFiles({ - 'src/string-script.js': "console.log('string-script'); var number = 1+1;", - 'src/pre-rename-script.js': "console.log('pre-rename-script');", - }); - - await updateJsonFile('angular.json', (configJson) => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.build.options.scripts = [ - { input: 'src/string-script.js' }, - { input: 'src/pre-rename-script.js', bundleName: 'renamed-script' }, - ]; - }); - - await ng( - 'build', - '--extract-css', - '--vendor-chunk', - '--optimization', - '--configuration=development', - ); - - // index.html lists the right bundles - await expectFileToMatch( - 'dist/test-project/index.html', - oneLineTrim` - - - - - - - - - - - `, - ); - - await expectFileToMatch('dist/test-project/vendor-es2017.js', /class \w{constructor\(/); - await expectToFail(() => - expectFileToMatch('dist/test-project/vendor-es5.js', /class \w{constructor\(/), - ); -} diff --git a/tests/legacy-cli/e2e/tests/build/polyfills.ts b/tests/legacy-cli/e2e/tests/build/polyfills.ts index b7b306057b68..50da87ca5d58 100644 --- a/tests/legacy-cli/e2e/tests/build/polyfills.ts +++ b/tests/legacy-cli/e2e/tests/build/polyfills.ts @@ -10,22 +10,19 @@ import { ng } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; export default async function () { - // Enable Differential loading to run both size checks + // Enable ES5 polyfills to run both size checks await appendToFile('.browserslistrc', 'IE 11'); await ng('build', '--aot=false', '--configuration=development'); // files were created successfully - await expectFileToMatch( - 'dist/test-project/polyfills-es5.js', - 'core-js/proposals/reflect-metadata', - ); + await expectFileToMatch('dist/test-project/polyfills-es5.js', 'core-js/es/reflect'); await expectFileToMatch('dist/test-project/polyfills-es5.js', 'zone.js'); await expectFileToMatch( 'dist/test-project/index.html', oneLineTrim` - -