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(); + }); + }); +});