From cdfa7ca88c2e79564192d4b7fdafb53d97f2607d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 30 Mar 2023 09:47:11 -0400 Subject: [PATCH] fix(@angular-devkit/build-angular): allow multiple polyfills with esbuild-based builder Previously when using the esbuild-based browser application, the `polyfills` option was limited to only one entry. The option now can be used with multiple entries and has full support for package resolution. This provides equivalent behavior to the current default Webpack-based builder. --- .../src/builders/browser-esbuild/index.ts | 36 +++++++++- .../src/builders/browser-esbuild/options.ts | 19 +---- .../tests/options/polyfills_spec.ts | 69 +++++++++++++++++++ 3 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index a5798732994d..c928e7465638 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -313,6 +313,7 @@ function createCodeBundleOptions( const { workspaceRoot, entryPoints, + polyfills, optimizationOptions, sourcemapOptions, tsconfig, @@ -327,7 +328,7 @@ function createCodeBundleOptions( tailwindConfiguration, } = options; - return { + const buildOptions: BuildOptions = { absWorkingDir: workspaceRoot, bundle: true, format: 'esm', @@ -391,6 +392,39 @@ function createCodeBundleOptions( 'ngJitMode': jit ? 'true' : 'false', }, }; + + if (polyfills?.length) { + const namespace = 'angular:polyfills'; + buildOptions.entryPoints = { + ...buildOptions.entryPoints, + ['polyfills']: namespace, + }; + + buildOptions.plugins?.unshift({ + name: 'angular-polyfills', + setup(build) { + build.onResolve({ filter: /^angular:polyfills$/ }, (args) => { + if (args.kind !== 'entry-point') { + return null; + } + + return { + path: 'entry', + namespace, + }; + }); + build.onLoad({ filter: /./, namespace }, () => { + return { + contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'), + loader: 'js', + resolveDir: workspaceRoot, + }; + }); + }, + }); + } + + return buildOptions; } /** diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts index aaff12a759bf..b592017ab01c 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts @@ -12,7 +12,6 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; -import { normalizePolyfills } from '../../utils/normalize-polyfills'; import { generateEntryPoints } from '../../utils/package-chunk-sort'; import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config'; import { normalizeGlobalStyles } from '../../webpack/utils/helpers'; @@ -46,19 +45,6 @@ export async function normalizeOptions( const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); const mainEntryPoint = path.join(workspaceRoot, options.main); - - // Currently esbuild do not support multiple files per entry-point - const [polyfillsEntryPoint, ...remainingPolyfills] = normalizePolyfills( - options.polyfills, - workspaceRoot, - ); - - if (remainingPolyfills.length) { - context.logger.warn( - `The 'polyfills' option currently does not support multiple entries by this experimental builder. The first entry will be used.`, - ); - } - const tsconfig = path.join(workspaceRoot, options.tsConfig); const outputPath = path.join(workspaceRoot, options.outputPath); const optimizationOptions = normalizeOptimization(options.optimization); @@ -133,9 +119,6 @@ export async function normalizeOptions( const entryPoints: Record = { main: mainEntryPoint, }; - if (polyfillsEntryPoint) { - entryPoints['polyfills'] = polyfillsEntryPoint; - } let indexHtmlOptions; if (options.index) { @@ -162,6 +145,7 @@ export async function normalizeOptions( extractLicenses, inlineStyleLanguage = 'css', poll, + polyfills, preserveSymlinks, statsJson, stylePreprocessorOptions, @@ -182,6 +166,7 @@ export async function normalizeOptions( inlineStyleLanguage, jit: !aot, stats: !!statsJson, + polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills], poll, // If not explicitly set, default to the Node.js process argument preserveSymlinks: preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'), diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts new file mode 100644 index 000000000000..27adcf879636 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/polyfills_spec.ts @@ -0,0 +1,69 @@ +/** + * @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 { buildEsbuildBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "polyfills"', () => { + it('uses a provided TypeScript file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/polyfills.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/polyfills.js').toExist(); + }); + + it('uses a provided JavaScript file', async () => { + await harness.writeFile('src/polyfills.js', `console.log('main');`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/polyfills.js', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/polyfills.js').content.toContain(`console.log("main")`); + }); + + it('fails and shows an error when file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/missing.ts', + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Could not resolve') }), + ); + + harness.expectFile('dist/polyfills.js').toNotExist(); + }); + + it('resolves module specifiers in array', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: ['zone.js', 'zone.js/testing'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('dist/polyfills.js').toExist(); + }); + }); +});