diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts index 6a18799333fe..a4f4882392a6 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts @@ -17,6 +17,7 @@ import { createCompilerPlugin } from './angular/compiler-plugin'; import { SourceFileCache } from './angular/source-file-cache'; import { BundlerOptionsFactory } from './bundler-context'; import { createCompilerPluginOptions } from './compiler-plugin-options'; +import { createExternalPackagesPlugin } from './external-packages-plugin'; import { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin'; @@ -59,14 +60,21 @@ export function createBrowserCodeBundleOptions( ], }; - if (options.externalPackages) { - buildOptions.packages = 'external'; - } - if (options.plugins) { buildOptions.plugins?.push(...options.plugins); } + if (options.externalPackages) { + // Package files affected by a customized loader should not be implicitly marked as external + if (options.loaderExtensions || options.plugins) { + // Plugin must be added after custom plugins to ensure any added loader options are considered + buildOptions.plugins?.push(createExternalPackagesPlugin()); + } else { + // Safe to use the packages external option directly + buildOptions.packages = 'external'; + } + } + return buildOptions; } diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/external-packages-plugin.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/external-packages-plugin.ts new file mode 100644 index 000000000000..a090503b3ee6 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/external-packages-plugin.ts @@ -0,0 +1,77 @@ +/** + * @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 type { Plugin } from 'esbuild'; +import { extname } from 'node:path'; + +const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION'); + +/** + * Creates a plugin that marks any resolved path as external if it is within a node modules directory. + * This is used instead of the esbuild `packages` option to avoid marking files that should be loaded + * via customized loaders. This is necessary to prevent Vite development server pre-bundling errors. + * + * @returns An esbuild plugin. + */ +export function createExternalPackagesPlugin(): Plugin { + return { + name: 'angular-external-packages', + setup(build) { + // Safe to use native packages external option if no loader options present + if ( + build.initialOptions.loader === undefined || + Object.keys(build.initialOptions.loader).length === 0 + ) { + build.initialOptions.packages = 'external'; + + return; + } + + const loaderFileExtensions = new Set(Object.keys(build.initialOptions.loader)); + + // Only attempt resolve of non-relative and non-absolute paths + build.onResolve({ filter: /^[^./]/ }, async (args) => { + if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) { + return null; + } + + const { importer, kind, resolveDir, namespace, pluginData = {} } = args; + pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true; + + const result = await build.resolve(args.path, { + importer, + kind, + namespace, + pluginData, + resolveDir, + }); + + // Return result if unable to resolve or explicitly marked external (externalDependencies option) + if (!result.path || result.external) { + return result; + } + + // Allow customized loaders to run against configured paths regardless of location + if (loaderFileExtensions.has(extname(result.path))) { + return result; + } + + // Mark paths from a node modules directory as external + if (/[\\/]node_modules[\\/]/.test(result.path)) { + return { + path: args.path, + external: true, + }; + } + + // Otherwise return original result + return result; + }); + }, + }; +}