diff --git a/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts index bd1208e37aa9..b1fa5dd8ae89 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/execute-build.ts @@ -12,6 +12,7 @@ import { createBrowserCodeBundleOptions, createBrowserPolyfillBundleOptions, createServerCodeBundleOptions, + createServerPolyfillBundleOptions, } from '../../tools/esbuild/application-code-bundle'; import { generateBudgetStats } from '../../tools/esbuild/budget-stats'; import { BuildOutputFileType, BundlerContext } from '../../tools/esbuild/bundler-context'; @@ -82,14 +83,14 @@ export async function executeBuild( ); // Browser polyfills code - const polyfillBundleOptions = createBrowserPolyfillBundleOptions( + const browserPolyfillBundleOptions = createBrowserPolyfillBundleOptions( options, target, codeBundleCache, ); - if (polyfillBundleOptions) { + if (browserPolyfillBundleOptions) { bundlerContexts.push( - new BundlerContext(workspaceRoot, !!options.watch, polyfillBundleOptions), + new BundlerContext(workspaceRoot, !!options.watch, browserPolyfillBundleOptions), ); } @@ -122,18 +123,36 @@ export async function executeBuild( } } - // Server application code // Skip server build when none of the features are enabled. if (serverEntryPoint && (prerenderOptions || appShellOptions || ssrOptions)) { - const nodeTargets = getSupportedNodeTargets(); + const nodeTargets = [...target, ...getSupportedNodeTargets()]; + // Server application code bundlerContexts.push( new BundlerContext( workspaceRoot, !!options.watch, - createServerCodeBundleOptions(options, [...target, ...nodeTargets], codeBundleCache), + createServerCodeBundleOptions(options, nodeTargets, codeBundleCache), () => false, ), ); + + // Server polyfills code + const serverPolyfillBundleOptions = createServerPolyfillBundleOptions( + options, + nodeTargets, + codeBundleCache, + ); + + if (serverPolyfillBundleOptions) { + bundlerContexts.push( + new BundlerContext( + workspaceRoot, + !!options.watch, + serverPolyfillBundleOptions, + () => false, + ), + ); + } } } diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts index 3f328e326e3c..2ed16591ab32 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.ts @@ -32,7 +32,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { }); describe('Behavior: "Rebuild both server and browser bundles when using lazy loading"', () => { - it('detect changes and errors when expected', async () => { + fit('detect changes and errors when expected', async () => { harness.useTarget('build', { ...BASE_OPTIONS, watch: true, @@ -45,7 +45,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const builderAbort = new AbortController(); const buildCount = await firstValueFrom( harness.execute({ outputLogsOnFailure: false, signal: builderAbort.signal }).pipe( - timeout(20_000), + timeout(30_000), concatMap(async ({ result, logs }, index) => { switch (index) { case 0: diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index 4e42a1b3b194..99500d16d6eb 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import remapping, { SourceMapInput } from '@ampproject/remapping'; import type { BuilderContext } from '@angular-devkit/architect'; import type { json, logging } from '@angular-devkit/core'; import type { Plugin } from 'esbuild'; @@ -72,6 +73,10 @@ export async function* serveWithVite( // This is so instead of prerendering all the routes for every change, the page is "prerendered" when it is requested. browserOptions.ssr = true; browserOptions.prerender = false; + + // https://nodejs.org/api/process.html#processsetsourcemapsenabledval + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (process as any).setSourceMapsEnabled(true); } // Set all packages as external to support Vite's prebundle caching @@ -403,6 +408,31 @@ export async function setupServer( }; }, configureServer(server) { + const originalssrTransform = server.ssrTransform; + server.ssrTransform = async (code, map, url, originalCode) => { + const result = await originalssrTransform(code, null, url, originalCode); + if (!result) { + return null; + } + + let transformedCode = result.code; + if (result.map && map) { + transformedCode += + `\n//# sourceMappingURL=` + + `data:application/json;base64,${Buffer.from( + JSON.stringify( + remapping([result.map as SourceMapInput, map as SourceMapInput], () => null), + ), + ).toString('base64')}`; + } + + return { + ...result, + map: null, + code: transformedCode, + }; + }; + // Assets and resources get handled first server.middlewares.use(function angularAssetsMiddleware(req, res, next) { if (req.url === undefined || res.writableEnded) { 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 fa81c7ffed0d..db6000da37af 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 @@ -10,7 +10,6 @@ import type { BuildOptions } from 'esbuild'; import assert from 'node:assert'; import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; import { extname, join, relative } from 'node:path'; import type { NormalizedApplicationBuildOptions } from '../../builders/application/options'; import { allowMangle } from '../../utils/environment-options'; @@ -18,6 +17,7 @@ import { createCompilerPlugin } from './angular/compiler-plugin'; import { SourceFileCache } from './angular/source-file-cache'; import { createCompilerPluginOptions } from './compiler-plugin-options'; import { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; +import { createJavaScriptTransformerPlugin } from './javascript-transfomer-plugin'; import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin'; import { getFeatureSupport } from './utils'; @@ -75,8 +75,13 @@ export function createBrowserPolyfillBundleOptions( target: string[], sourceFileCache?: SourceFileCache, ): BuildOptions | undefined { - const { workspaceRoot, outputNames, jit } = options; + const namespace = 'angular:polyfills'; + const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions(options, namespace, true); + if (!polyfillBundleOptions) { + return; + } + const { outputNames } = options; const { pluginOptions, styleOptions } = createCompilerPluginOptions( options, target, @@ -84,7 +89,7 @@ export function createBrowserPolyfillBundleOptions( ); const buildOptions: BuildOptions = { - ...getEsBuildCommonOptions(options), + ...polyfillBundleOptions, 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 @@ -93,121 +98,19 @@ export function createBrowserPolyfillBundleOptions( mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'], entryNames: outputNames.bundles, target, - splitting: false, - supported: getFeatureSupport(target), - plugins: [ - createSourcemapIgnorelistPlugin(), - createCompilerPlugin( - // JS/TS options - { ...pluginOptions, noopTypeScriptCompilation: true }, - // Component stylesheet options are unused for polyfills but required by the plugin - styleOptions, - ), - ], - }; - buildOptions.plugins ??= []; - - const polyfills = options.polyfills ? [...options.polyfills] : []; - - // Angular JIT mode requires the runtime compiler - if (jit) { - polyfills.push('@angular/compiler'); - } - - // Add Angular's global locale data if i18n options are present. - // Locale data should go first so that project provided polyfill code can augment if needed. - let needLocaleDataPlugin = false; - if (options.i18nOptions.shouldInline) { - // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier - polyfills.unshift('angular:locale/placeholder'); - buildOptions.plugins?.unshift( - createVirtualModulePlugin({ - namespace: 'angular:locale/placeholder', - entryPointOnly: false, - loadContent: () => ({ - contents: `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n`, - loader: 'js', - resolveDir: workspaceRoot, - }), - }), - ); - - // Add locale data for all active locales - // TODO: Inject each individually within the inlining process itself - for (const locale of options.i18nOptions.inlineLocales) { - polyfills.unshift(`angular:locale/data:${locale}`); - } - needLocaleDataPlugin = true; - } else if (options.i18nOptions.hasDefinedSourceLocale) { - // When not inlining and a source local is present, use the source locale data directly - polyfills.unshift(`angular:locale/data:${options.i18nOptions.sourceLocale}`); - needLocaleDataPlugin = true; - } - if (needLocaleDataPlugin) { - buildOptions.plugins?.push(createAngularLocaleDataPlugin()); - } - - if (polyfills.length === 0) { - return; - } - - // Add polyfill entry point if polyfills are present - const namespace = 'angular:polyfills'; - buildOptions.entryPoints = { - 'polyfills': namespace, + entryPoints: { + 'polyfills': namespace, + }, }; - buildOptions.plugins?.unshift( - createVirtualModulePlugin({ - namespace, - loadContent: async (_, build) => { - let hasLocalizePolyfill = false; - const polyfillPaths = await Promise.all( - polyfills.map(async (path) => { - hasLocalizePolyfill ||= path.startsWith('@angular/localize'); - - if (path.startsWith('zone.js') || !extname(path)) { - return path; - } - - const potentialPathRelative = './' + path; - const result = await build.resolve(potentialPathRelative, { - kind: 'import-statement', - resolveDir: workspaceRoot, - }); - - return result.path ? potentialPathRelative : path; - }), - ); - - if (!options.i18nOptions.shouldInline && !hasLocalizePolyfill) { - // Cannot use `build.resolve` here since it does not allow overriding the external options - // and the actual presence of the `@angular/localize` package needs to be checked here. - const workspaceRequire = createRequire(workspaceRoot + '/'); - try { - workspaceRequire.resolve('@angular/localize'); - // The resolve call above will throw if not found - polyfillPaths.push('@angular/localize/init'); - } catch {} - } - - // Generate module contents with an import statement per defined polyfill - let contents = polyfillPaths - .map((file) => `import '${file.replace(/\\/g, '/')}';`) - .join('\n'); - - // If not inlining translations and source locale is defined, inject the locale specifier - if (!options.i18nOptions.shouldInline && options.i18nOptions.hasDefinedSourceLocale) { - contents += `(globalThis.$localize ??= {}).locale = "${options.i18nOptions.sourceLocale}";\n`; - } - - return { - contents, - loader: 'js', - resolveDir: workspaceRoot, - }; - }, - }), + buildOptions.plugins ??= []; + buildOptions.plugins.push( + createCompilerPlugin( + // JS/TS options + { ...pluginOptions, noopTypeScriptCompilation: true }, + // Component stylesheet options are unused for polyfills but required by the plugin + styleOptions, + ), ); return buildOptions; @@ -246,7 +149,6 @@ export function createServerCodeBundleOptions( const mainServerNamespace = 'angular:main-server'; const ssrEntryNamespace = 'angular:ssr-entry'; - const entryPoints: Record = { 'main.server': mainServerNamespace, }; @@ -259,8 +161,7 @@ export function createServerCodeBundleOptions( const buildOptions: BuildOptions = { ...getEsBuildCommonOptions(options), platform: 'node', - // TODO: Invesigate why enabling `splitting` in JIT mode causes an "'@angular/compiler' is not available" error. - splitting: !jit, + splitting: true, outExtension: { '.js': '.mjs' }, // 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 @@ -268,6 +169,7 @@ export function createServerCodeBundleOptions( // More details: https://github.com/angular/angular-cli/issues/25405. mainFields: ['es2020', 'es2015', 'module', 'main'], entryNames: '[name]', + external: ['./polyfills.server.mjs'], target, banner: { // Note: Needed as esbuild does not provide require shims / proxy from ESModules. @@ -275,6 +177,8 @@ export function createServerCodeBundleOptions( js: [ `import { createRequire } from 'node:module';`, `globalThis['require'] ??= createRequire(import.meta.url);`, + // During JIT mode the polyfills need to be included here as @angular/compiler need to be loaded the very first thing. + jit ? `import './polyfills.server.mjs';` : '', ].join('\n'), }, entryPoints, @@ -297,74 +201,28 @@ export function createServerCodeBundleOptions( buildOptions.plugins.push(createRxjsEsmResolutionPlugin()); } - const polyfills: string[] = []; - if (options.polyfills?.includes('zone.js')) { - polyfills.push(`import 'zone.js/node';`); - } - - if (jit) { - polyfills.push(`import '@angular/compiler';`); - } - - polyfills.push(`import '@angular/platform-server/init';`); - - // Add Angular's global locale data if i18n options are present. - let needLocaleDataPlugin = false; - if (options.i18nOptions.shouldInline) { - // Add locale data for all active locales - for (const locale of options.i18nOptions.inlineLocales) { - polyfills.unshift(`import 'angular:locale/data:${locale}';`); - } - needLocaleDataPlugin = true; - } else if (options.i18nOptions.hasDefinedSourceLocale) { - // When not inlining and a source local is present, use the source locale data directly - polyfills.unshift(`import 'angular:locale/data:${options.i18nOptions.sourceLocale}';`); - needLocaleDataPlugin = true; - } - if (needLocaleDataPlugin) { - buildOptions.plugins.push(createAngularLocaleDataPlugin()); - } - buildOptions.plugins.push( createVirtualModulePlugin({ namespace: mainServerNamespace, loadContent: async () => { const mainServerEntryPoint = relative(workspaceRoot, serverEntryPoint).replace(/\\/g, '/'); + const contents: string[] = []; + if (!jit) { + contents.push(`import './polyfills.server.mjs';`); + } - const contents = [ - ...polyfills, + contents.push( `import moduleOrBootstrapFn from './${mainServerEntryPoint}';`, `export default moduleOrBootstrapFn;`, `export * from './${mainServerEntryPoint}';`, `export { ɵConsole } from '@angular/core';`, `export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`, - ]; + ); if (watch) { contents.push(`export { ɵresetCompiledComponents } from '@angular/core';`); } - if (!options.i18nOptions.shouldInline) { - // Cannot use `build.resolve` here since it does not allow overriding the external options - // and the actual presence of the `@angular/localize` package needs to be checked here. - const workspaceRequire = createRequire(workspaceRoot + '/'); - try { - workspaceRequire.resolve('@angular/localize'); - // The resolve call above will throw if not found - contents.push(`import '@angular/localize/init';`); - } catch {} - } - - if (options.i18nOptions.shouldInline) { - // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier - contents.push('(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";'); - } else if (options.i18nOptions.hasDefinedSourceLocale) { - // If not inlining translations and source locale is defined, inject the locale specifier - contents.push( - `(globalThis.$localize ??= {}).locale = "${options.i18nOptions.sourceLocale}";`, - ); - } - if (prerenderOptions?.discoverRoutes) { // We do not import it directly so that node.js modules are resolved using the correct context. const routesExtractorCode = await readFile( @@ -391,13 +249,16 @@ export function createServerCodeBundleOptions( namespace: ssrEntryNamespace, loadContent: () => { const serverEntryPoint = relative(workspaceRoot, ssrEntryPoint).replace(/\\/g, '/'); + const contents: string[] = [ + `import './${serverEntryPoint}';`, + `export * from './${serverEntryPoint}';`, + ]; + if (!jit) { + contents.unshift(`import './polyfills.server.mjs';`); + } return { - contents: [ - ...polyfills, - `import './${serverEntryPoint}';`, - `export * from './${serverEntryPoint}';`, - ].join('\n'), + contents: contents.join('\n'), loader: 'js', resolveDir: workspaceRoot, }; @@ -413,6 +274,91 @@ export function createServerCodeBundleOptions( return buildOptions; } +export function createServerPolyfillBundleOptions( + options: NormalizedApplicationBuildOptions, + target: string[], + sourceFileCache?: SourceFileCache, +): BuildOptions | undefined { + const polyfills: string[] = []; + const zoneFlagsNamespace = 'angular:zone-flags/placeholder'; + const polyfillsFromConfig = new Set(options.polyfills); + let hasZoneJs = false; + + if (polyfillsFromConfig.has('zone.js')) { + hasZoneJs = true; + polyfills.push(zoneFlagsNamespace, 'zone.js/node'); + } + + if ( + polyfillsFromConfig.has('@angular/localize') || + polyfillsFromConfig.has('@angular/localize/init') + ) { + polyfills.push('@angular/localize/init'); + } + + polyfills.push('@angular/platform-server/init'); + + const namespace = 'angular:polyfills-server'; + const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions( + { + ...options, + polyfills, + }, + namespace, + false, + ); + + if (!polyfillBundleOptions) { + return; + } + + const { workspaceRoot, jit, sourcemapOptions, advancedOptimizations } = options; + const buildOptions: BuildOptions = { + ...polyfillBundleOptions, + platform: 'node', + outExtension: { '.js': '.mjs' }, + // 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', 'module', 'main'], + entryNames: '[name]', + target, + entryPoints: { + 'polyfills.server': namespace, + }, + }; + + buildOptions.plugins ??= []; + + // Disable Zone.js uncaught promise rejections to provide cleaner stacktraces. + if (hasZoneJs) { + buildOptions.plugins.unshift( + createVirtualModulePlugin({ + namespace: zoneFlagsNamespace, + entryPointOnly: false, + loadContent: () => ({ + contents: `globalThis.__zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION = true;`, + loader: 'js', + resolveDir: workspaceRoot, + }), + }), + ); + } + + buildOptions.plugins.push( + createJavaScriptTransformerPlugin({ + jit, + sourcemap: !!sourcemapOptions.scripts, + babelFileCache: sourceFileCache?.babelFileCache, + advancedOptimizations, + maxWorkers: 1, + }), + ); + + return buildOptions; +} + function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): BuildOptions { const { workspaceRoot, @@ -475,3 +421,120 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu publicPath: options.publicPath, }; } + +function getEsBuildCommonPolyfillsOptions( + options: NormalizedApplicationBuildOptions, + namespace: string, + tryToResolvePolyfillsAsRelative: boolean, +): BuildOptions | undefined { + const { jit, workspaceRoot, i18nOptions } = options; + const buildOptions: BuildOptions = { + ...getEsBuildCommonOptions(options), + splitting: false, + plugins: [createSourcemapIgnorelistPlugin()], + }; + + const polyfills = options.polyfills ? [...options.polyfills] : []; + + // Angular JIT mode requires the runtime compiler + if (jit) { + polyfills.unshift('@angular/compiler'); + } + + // Add Angular's global locale data if i18n options are present. + // Locale data should go first so that project provided polyfill code can augment if needed. + let needLocaleDataPlugin = false; + if (i18nOptions.shouldInline) { + // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier + polyfills.unshift('angular:locale/placeholder'); + buildOptions.plugins?.push( + createVirtualModulePlugin({ + namespace: 'angular:locale/placeholder', + entryPointOnly: false, + loadContent: () => ({ + contents: `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n`, + loader: 'js', + resolveDir: workspaceRoot, + }), + }), + ); + + // Add locale data for all active locales + // TODO: Inject each individually within the inlining process itself + for (const locale of i18nOptions.inlineLocales) { + polyfills.unshift(`angular:locale/data:${locale}`); + } + needLocaleDataPlugin = true; + } else if (i18nOptions.hasDefinedSourceLocale) { + // When not inlining and a source local is present, use the source locale data directly + polyfills.unshift(`angular:locale/data:${i18nOptions.sourceLocale}`); + needLocaleDataPlugin = true; + } + if (needLocaleDataPlugin) { + buildOptions.plugins?.push(createAngularLocaleDataPlugin()); + } + + if (polyfills.length === 0) { + return; + } + + buildOptions.plugins?.push( + createVirtualModulePlugin({ + namespace, + loadContent: async (_, build) => { + let hasLocalizePolyfill = false; + let polyfillPaths = polyfills; + + if (tryToResolvePolyfillsAsRelative) { + polyfillPaths = await Promise.all( + polyfills.map(async (path) => { + hasLocalizePolyfill ||= path.startsWith('@angular/localize'); + if (path.startsWith('zone.js') || !extname(path)) { + return path; + } + + const potentialPathRelative = './' + path; + const result = await build.resolve(potentialPathRelative, { + kind: 'import-statement', + resolveDir: workspaceRoot, + }); + + return result.path ? potentialPathRelative : path; + }), + ); + } else { + hasLocalizePolyfill = polyfills.some((p) => p.startsWith('@angular/localize')); + } + + if (!i18nOptions.shouldInline && !hasLocalizePolyfill) { + const result = await build.resolve('@angular/localize', { + kind: 'import-statement', + resolveDir: workspaceRoot, + }); + + if (result.path) { + polyfillPaths.push('@angular/localize/init'); + } + } + + // Generate module contents with an import statement per defined polyfill + let contents = polyfillPaths + .map((file) => `import '${file.replace(/\\/g, '/')}';`) + .join('\n'); + + // If not inlining translations and source locale is defined, inject the locale specifier + if (!i18nOptions.shouldInline && i18nOptions.hasDefinedSourceLocale) { + contents += `(globalThis.$localize ??= {}).locale = "${i18nOptions.sourceLocale}";\n`; + } + + return { + contents, + loader: 'js', + resolveDir: workspaceRoot, + }; + }, + }), + ); + + return buildOptions; +} diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transfomer-plugin.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transfomer-plugin.ts new file mode 100644 index 000000000000..fd8d36e27384 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transfomer-plugin.ts @@ -0,0 +1,71 @@ +/** + * @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 { JavaScriptTransformer, JavaScriptTransformerOptions } from './javascript-transformer'; + +export interface JavaScriptTransformerPluginOptions extends JavaScriptTransformerOptions { + babelFileCache?: Map; + maxWorkers: number; +} + +/** + * Creates a plugin that Transformers JavaScript using Babel. + * + * @returns An esbuild plugin. + */ +export function createJavaScriptTransformerPlugin( + options: JavaScriptTransformerPluginOptions, +): Plugin { + return { + name: 'angular-javascript-transformer', + setup(build) { + let javascriptTransformer: JavaScriptTransformer | undefined; + const { + sourcemap, + thirdPartySourcemaps, + advancedOptimizations, + jit, + babelFileCache, + maxWorkers, + } = options; + + build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => { + // The filename is currently used as a cache key. Since the cache is memory only, + // the options cannot change and do not need to be represented in the key. If the + // cache is later stored to disk, then the options that affect transform output + // would need to be added to the key as well as a check for any change of content. + let contents = babelFileCache?.get(args.path); + if (contents === undefined) { + // Initialize a worker pool for JavaScript transformations + javascriptTransformer ??= new JavaScriptTransformer( + { + sourcemap, + thirdPartySourcemaps, + advancedOptimizations, + jit, + }, + maxWorkers, + ); + + contents = await javascriptTransformer.transformFile(args.path, jit); + babelFileCache?.set(args.path, contents); + } + + return { + contents, + loader: 'js', + }; + }); + + build.onDispose(() => { + void javascriptTransformer?.close(); + }); + }, + }; +} diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts index 0c944ccacbb6..3aff16162373 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts @@ -29,7 +29,7 @@ export class JavaScriptTransformer { #workerPool: Piscina; #commonOptions: Required; - constructor(options: JavaScriptTransformerOptions, maxThreads?: number) { + constructor(options: JavaScriptTransformerOptions, maxThreads: number) { this.#workerPool = new Piscina({ filename: require.resolve('./javascript-transformer-worker'), maxThreads,