diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index 323e3783439d..b969253e703f 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -126,7 +126,7 @@ export function setupBundlerContexts( const serverPolyfillBundleOptions = createServerPolyfillBundleOptions( options, nodeTargets, - codeBundleCache, + codeBundleCache.loadResultCache, ); if (serverPolyfillBundleOptions) { diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index 38c0e864233a..15ec37e0125a 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -38,62 +38,64 @@ export function createBrowserCodeBundleOptions( target: string[], sourceFileCache: SourceFileCache, stylesheetBundler: ComponentStylesheetBundler, -): BuildOptions { - const { entryPoints, outputNames, polyfills } = options; - - const pluginOptions = createCompilerPluginOptions(options, sourceFileCache); - - const zoneless = isZonelessApp(polyfills); - - const buildOptions: BuildOptions = { - ...getEsBuildCommonOptions(options), - platform: 'browser', - // Note: `es2015` is needed for RxJS v6. If not specified, `module` would - // match and the ES5 distribution would be bundled and ends up breaking at - // runtime with the RxJS testing library. - // More details: https://github.com/angular/angular-cli/issues/25405. - mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'], - entryNames: outputNames.bundles, - entryPoints, - target, - supported: getFeatureSupport(target, zoneless), - plugins: [ - createLoaderImportAttributePlugin(), - createWasmPlugin({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }), - createSourcemapIgnorelistPlugin(), - createCompilerPlugin( - // JS/TS options - pluginOptions, - // Component stylesheet bundler - stylesheetBundler, - ), - ], - }; +): BundlerOptionsFactory { + return (loadCache) => { + const { entryPoints, outputNames, polyfills } = options; + + const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadCache); + + const zoneless = isZonelessApp(polyfills); + + const buildOptions: BuildOptions = { + ...getEsBuildCommonOptions(options), + platform: 'browser', + // Note: `es2015` is needed for RxJS v6. If not specified, `module` would + // match and the ES5 distribution would be bundled and ends up breaking at + // runtime with the RxJS testing library. + // More details: https://github.com/angular/angular-cli/issues/25405. + mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'], + entryNames: outputNames.bundles, + entryPoints, + target, + supported: getFeatureSupport(target, zoneless), + plugins: [ + createLoaderImportAttributePlugin(), + createWasmPlugin({ allowAsync: zoneless, cache: loadCache }), + createSourcemapIgnorelistPlugin(), + createCompilerPlugin( + // JS/TS options + pluginOptions, + // Component stylesheet bundler + stylesheetBundler, + ), + ], + }; - if (options.plugins) { - buildOptions.plugins?.push(...options.plugins); - } + 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 || - typeof options.externalPackages === 'object' - ) { - // Plugin must be added after custom plugins to ensure any added loader options are considered - buildOptions.plugins?.push( - createExternalPackagesPlugin( - options.externalPackages !== true ? options.externalPackages : undefined, - ), - ); - } else { - // Safe to use the packages external option directly - buildOptions.packages = 'external'; + if (options.externalPackages) { + // Package files affected by a customized loader should not be implicitly marked as external + if ( + options.loaderExtensions || + options.plugins || + typeof options.externalPackages === 'object' + ) { + // Plugin must be added after custom plugins to ensure any added loader options are considered + buildOptions.plugins?.push( + createExternalPackagesPlugin( + options.externalPackages !== true ? options.externalPackages : undefined, + ), + ); + } else { + // Safe to use the packages external option directly + buildOptions.packages = 'external'; + } } - } - return buildOptions; + return buildOptions; + }; } export function createBrowserPolyfillBundleOptions( @@ -158,7 +160,7 @@ export function createBrowserPolyfillBundleOptions( export function createServerPolyfillBundleOptions( options: NormalizedApplicationBuildOptions, target: string[], - sourceFileCache?: SourceFileCache, + loadResultCache: LoadResultCache | undefined, ): BundlerOptionsFactory | undefined { const serverPolyfills: string[] = []; const polyfillsFromConfig = new Set(options.polyfills); @@ -185,7 +187,7 @@ export function createServerPolyfillBundleOptions( }, namespace, false, - sourceFileCache?.loadResultCache, + loadResultCache, ); if (!polyfillBundleOptions) { @@ -372,7 +374,7 @@ export function createSsrEntryCodeBundleOptions( target: string[], sourceFileCache: SourceFileCache, stylesheetBundler: ComponentStylesheetBundler, -): BuildOptions { +): BundlerOptionsFactory { const { workspaceRoot, ssrOptions, externalPackages } = options; const serverEntryPoint = ssrOptions?.entry; assert( @@ -380,129 +382,131 @@ export function createSsrEntryCodeBundleOptions( 'createSsrEntryCodeBundleOptions should not be called without a defined serverEntryPoint.', ); - const pluginOptions = createCompilerPluginOptions(options, sourceFileCache); - - const ssrEntryNamespace = 'angular:ssr-entry'; - const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest'; - const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require'; - const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; - - const inject: string[] = [ssrInjectManifestNamespace]; - if (isNodePlatform) { - inject.unshift(ssrInjectRequireNamespace); - } - - const buildOptions: BuildOptions = { - ...getEsBuildServerCommonOptions(options), - target, - entryPoints: { - // TODO: consider renaming to index - 'server': ssrEntryNamespace, - }, - supported: getFeatureSupport(target, true), - plugins: [ - createSourcemapIgnorelistPlugin(), - createCompilerPlugin( - // JS/TS options - { ...pluginOptions, noopTypeScriptCompilation: true }, - // Component stylesheet bundler - stylesheetBundler, - ), - ], - inject, - }; - - buildOptions.plugins ??= []; + return (loadResultCache) => { + const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadResultCache); - if (externalPackages) { - buildOptions.packages = 'external'; - } else { - buildOptions.plugins.push(createRxjsEsmResolutionPlugin()); - } + const ssrEntryNamespace = 'angular:ssr-entry'; + const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest'; + const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require'; + const isNodePlatform = options.ssrOptions?.platform !== ExperimentalPlatform.Neutral; - // Mark manifest file as external. As this will be generated later on. - (buildOptions.external ??= []).push('*/main.server.mjs', ...SERVER_GENERATED_EXTERNALS); + const inject: string[] = [ssrInjectManifestNamespace]; + if (isNodePlatform) { + inject.unshift(ssrInjectRequireNamespace); + } - if (!isNodePlatform) { - // `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client. - // Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms. - // Note: The framework already issues a warning when using XHR with SSR. - buildOptions.external.push('xhr2'); - } + const buildOptions: BuildOptions = { + ...getEsBuildServerCommonOptions(options), + target, + entryPoints: { + // TODO: consider renaming to index + 'server': ssrEntryNamespace, + }, + supported: getFeatureSupport(target, true), + plugins: [ + createSourcemapIgnorelistPlugin(), + createCompilerPlugin( + // JS/TS options + { ...pluginOptions, noopTypeScriptCompilation: true }, + // Component stylesheet bundler + stylesheetBundler, + ), + ], + inject, + }; - buildOptions.plugins.push( - createServerBundleMetadata({ ssrEntryBundle: true }), - createVirtualModulePlugin({ - namespace: ssrInjectRequireNamespace, - cache: sourceFileCache?.loadResultCache, - loadContent: () => { - const contents: string[] = [ - // Note: Needed as esbuild does not provide require shims / proxy from ESModules. - // See: https://github.com/evanw/esbuild/issues/1921. - `import { createRequire } from 'node:module';`, - `globalThis['require'] ??= createRequire(import.meta.url);`, - ]; + buildOptions.plugins ??= []; - return { - contents: contents.join('\n'), - loader: 'js', - resolveDir: workspaceRoot, - }; - }, - }), - createVirtualModulePlugin({ - namespace: ssrInjectManifestNamespace, - cache: sourceFileCache?.loadResultCache, - loadContent: () => { - const contents: string[] = [ - // Configure `@angular/ssr` app engine manifest. - `import manifest from './${SERVER_APP_ENGINE_MANIFEST_FILENAME}';`, - `import { ɵsetAngularAppEngineManifest } from '@angular/ssr';`, - `ɵsetAngularAppEngineManifest(manifest);`, - ]; + if (externalPackages) { + buildOptions.packages = 'external'; + } else { + buildOptions.plugins.push(createRxjsEsmResolutionPlugin()); + } - return { - contents: contents.join('\n'), - loader: 'js', - resolveDir: workspaceRoot, - }; - }, - }), - createVirtualModulePlugin({ - namespace: ssrEntryNamespace, - cache: sourceFileCache?.loadResultCache, - loadContent: () => { - const serverEntryPointJsImport = entryFileToWorkspaceRelative( - workspaceRoot, - serverEntryPoint, - ); - const contents: string[] = [ - // Re-export all symbols including default export - `import * as server from '${serverEntryPointJsImport}';`, - `export * from '${serverEntryPointJsImport}';`, - // The below is needed to avoid - // `Import "default" will always be undefined because there is no matching export` warning when no default is present. - `const defaultExportName = 'default';`, - `export default server[defaultExportName]`, + // Mark manifest file as external. As this will be generated later on. + (buildOptions.external ??= []).push('*/main.server.mjs', ...SERVER_GENERATED_EXTERNALS); - // Add @angular/ssr exports - `export { AngularAppEngine } from '@angular/ssr';`, - ]; + if (!isNodePlatform) { + // `@angular/platform-server` lazily depends on `xhr2` for XHR usage with the HTTP client. + // Since `xhr2` has Node.js dependencies, it cannot be used when targeting non-Node.js platforms. + // Note: The framework already issues a warning when using XHR with SSR. + buildOptions.external.push('xhr2'); + } - return { - contents: contents.join('\n'), - loader: 'js', - resolveDir: workspaceRoot, - }; - }, - }), - ); + buildOptions.plugins.push( + createServerBundleMetadata({ ssrEntryBundle: true }), + createVirtualModulePlugin({ + namespace: ssrInjectRequireNamespace, + cache: loadResultCache, + loadContent: () => { + const contents: string[] = [ + // Note: Needed as esbuild does not provide require shims / proxy from ESModules. + // See: https://github.com/evanw/esbuild/issues/1921. + `import { createRequire } from 'node:module';`, + `globalThis['require'] ??= createRequire(import.meta.url);`, + ]; + + return { + contents: contents.join('\n'), + loader: 'js', + resolveDir: workspaceRoot, + }; + }, + }), + createVirtualModulePlugin({ + namespace: ssrInjectManifestNamespace, + cache: loadResultCache, + loadContent: () => { + const contents: string[] = [ + // Configure `@angular/ssr` app engine manifest. + `import manifest from './${SERVER_APP_ENGINE_MANIFEST_FILENAME}';`, + `import { ɵsetAngularAppEngineManifest } from '@angular/ssr';`, + `ɵsetAngularAppEngineManifest(manifest);`, + ]; + + return { + contents: contents.join('\n'), + loader: 'js', + resolveDir: workspaceRoot, + }; + }, + }), + createVirtualModulePlugin({ + namespace: ssrEntryNamespace, + cache: loadResultCache, + loadContent: () => { + const serverEntryPointJsImport = entryFileToWorkspaceRelative( + workspaceRoot, + serverEntryPoint, + ); + const contents: string[] = [ + // Re-export all symbols including default export + `import * as server from '${serverEntryPointJsImport}';`, + `export * from '${serverEntryPointJsImport}';`, + // The below is needed to avoid + // `Import "default" will always be undefined because there is no matching export` warning when no default is present. + `const defaultExportName = 'default';`, + `export default server[defaultExportName]`, + + // Add @angular/ssr exports + `export { AngularAppEngine } from '@angular/ssr';`, + ]; + + return { + contents: contents.join('\n'), + loader: 'js', + resolveDir: workspaceRoot, + }; + }, + }), + ); - if (options.plugins) { - buildOptions.plugins.push(...options.plugins); - } + if (options.plugins) { + buildOptions.plugins.push(...options.plugins); + } - return buildOptions; + return buildOptions; + }; } function getEsBuildServerCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions { diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index 15164146b9c8..03a82172b967 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -155,10 +155,6 @@ export class ExecutionResult { // To ensure path comparisons are valid, all these paths must be normalized. files.push(...this.codeBundleCache.referencedFiles.map(normalize)); } - if (this.codeBundleCache?.loadResultCache) { - // Load result caches internally normalize file dependencies - files.push(...this.codeBundleCache.loadResultCache.watchFiles); - } return files.concat(this.extraWatchFiles); } diff --git a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts index 7f8e496a3383..f29c71e0be2b 100644 --- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts +++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts @@ -8,14 +8,15 @@ import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import type { createCompilerPlugin } from './angular/compiler-plugin'; -import { ComponentStylesheetBundler } from './angular/component-stylesheets'; import type { SourceFileCache } from './angular/source-file-cache'; +import type { LoadResultCache } from './load-result-cache'; type CreateCompilerPluginParameters = Parameters; export function createCompilerPluginOptions( options: NormalizedApplicationBuildOptions, - sourceFileCache?: SourceFileCache, + sourceFileCache: SourceFileCache, + loadResultCache?: LoadResultCache, ): CreateCompilerPluginParameters[0] { const { sourcemapOptions, @@ -37,7 +38,7 @@ export function createCompilerPluginOptions( advancedOptimizations, fileReplacements, sourceFileCache, - loadResultCache: sourceFileCache?.loadResultCache, + loadResultCache, incremental, externalRuntimeStyles, instrumentForCoverage,