diff --git a/packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts index 87df19ee81eb..4c6927cb0e82 100644 --- a/packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts +++ b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts @@ -19,7 +19,6 @@ import { loadInputSourceMapFromUrl, removeSourceMappingURL, } from '../../utils/source-map'; -import { transform as transformWithOxc } from '../oxc/oxc-transform.js'; import type { JavaScriptTransformerOptions } from './javascript-transformer'; interface JavaScriptTransformRequest { @@ -54,6 +53,8 @@ let babelLinkerDeps: let remapping: typeof import('@ampproject/remapping').default | undefined; +let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined; + const textDecoder = new TextDecoder(); const textEncoder = new TextEncoder(); @@ -181,7 +182,18 @@ async function transformJavaScriptImpl( coverageMap = result.map; } - if (shouldLink && useBabelLinker) { + let oxcLink = shouldLink && !useBabelLinker; + const shouldRunOxc = oxcLink || (!process.versions.webcontainer && advancedOptimizations); + + if (shouldRunOxc) { + try { + oxcTransform ??= (await import('../oxc/oxc-transform.js')).transform; + } catch { + oxcLink = false; + } + } + + if (shouldLink && (useBabelLinker || !oxcLink)) { babelLinkerDeps ??= Promise.all([ import('@angular/compiler-cli/linker/babel'), import('@angular/compiler-cli'), @@ -222,14 +234,13 @@ async function transformJavaScriptImpl( } // Run Oxc linking and/or advanced optimizations in a single unified AST traversal pass - const oxcLink = shouldLink && !useBabelLinker; - if (oxcLink || advancedOptimizations) { + if (oxcTransform && (oxcLink || advancedOptimizations)) { const sideEffectFree = options.sideEffects === false; const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); const topLevelSafeMode = !safeAngularPackage; - const result = transformWithOxc(filename, code, { + const result = oxcTransform(filename, code, { link: oxcLink, jit, advancedOptimizations, diff --git a/packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts index 94d671ef2a72..f87153d3f706 100644 --- a/packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts +++ b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts @@ -343,6 +343,39 @@ describe('JavaScriptTransformer sourcemaps', () => { expect(text).not.toContain('i0.ɵɵngDeclareDirective'); }); + it('should link declarations using the Babel linker when NG_BUILD_BABEL_LINKER is enabled', async () => { + process.env['NG_BUILD_BABEL_LINKER'] = 'true'; + try { + transformer = new JavaScriptTransformer({ + sourcemap: false, + maxConcurrency: 1, + }); + + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + `; + + const result = await transformer.transformData( + 'node_modules/@angular/compiler-cli/test.js', + input, + { skipLinker: false }, + ); + const text = Buffer.from(result).toString('utf-8'); + + expect(text).not.toContain('i0.ɵɵngDeclareDirective'); + } finally { + delete process.env['NG_BUILD_BABEL_LINKER']; + } + }); + describe('advanced optimizations fast-path pre-filter', () => { it('should bypass worker and return input buffer directly when no candidate tokens are present', async () => { transformer = new JavaScriptTransformer({ diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index d87c104aba65..7f6e5b785f38 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -218,11 +218,14 @@ export const useComponentTemplateHmr = parseTristate(process.env['NG_HMR_TEMPLAT export const usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SSR']) === true; /** - * When `NG_BUILD_BABEL_LINKER` is enabled (`1` or `true`), the Babel-based + * When `NG_BUILD_BABEL_LINKER` is enabled (`1` or `true`), or when running within a + * WebContainer environment (unless explicitly disabled), the Babel-based * Angular Linker (`@angular/compiler-cli/linker/babel`) will be used instead of the * default OXC in-place linker. */ -export const useBabelLinker = parseTristate(process.env['NG_BUILD_BABEL_LINKER']) === true; +export const useBabelLinker = process.versions.webcontainer + ? parseTristate(process.env['NG_BUILD_BABEL_LINKER']) !== false + : parseTristate(process.env['NG_BUILD_BABEL_LINKER']) === true; /** * When `NG_BUILD_SASS_EMBEDDED` is set to `0` or `false`, or when running within a diff --git a/packages/angular/build/src/utils/environment-options_spec.ts b/packages/angular/build/src/utils/environment-options_spec.ts index 4ad098a24763..c66b8b1e2926 100644 --- a/packages/angular/build/src/utils/environment-options_spec.ts +++ b/packages/angular/build/src/utils/environment-options_spec.ts @@ -245,3 +245,60 @@ describe('environment options - maxInlinerWorkers', () => { expect(maxInlinerWorkers).toBe(Math.min(8, availableParallelism())); }); }); + +describe('environment options - useBabelLinker', () => { + const originalEnvValue = process.env['NG_BUILD_BABEL_LINKER']; + const originalWebcontainer = process.versions.webcontainer; + + function loadEnvironmentOptions(): typeof import('./environment-options') { + delete require.cache[require.resolve('./environment-options')]; + + return require('./environment-options'); + } + + afterEach(() => { + if (originalEnvValue !== undefined) { + process.env['NG_BUILD_BABEL_LINKER'] = originalEnvValue; + } else { + delete process.env['NG_BUILD_BABEL_LINKER']; + } + if (originalWebcontainer !== undefined) { + process.versions.webcontainer = originalWebcontainer; + } else { + delete process.versions.webcontainer; + } + delete require.cache[require.resolve('./environment-options')]; + }); + + it('defaults useBabelLinker to false in a standard environment', () => { + delete process.env['NG_BUILD_BABEL_LINKER']; + delete process.versions.webcontainer; + const { useBabelLinker } = loadEnvironmentOptions(); + + expect(useBabelLinker).toBeFalse(); + }); + + it('defaults useBabelLinker to true in a webcontainer environment', () => { + delete process.env['NG_BUILD_BABEL_LINKER']; + process.versions.webcontainer = '1.0.0'; + const { useBabelLinker } = loadEnvironmentOptions(); + + expect(useBabelLinker).toBeTrue(); + }); + + it('allows disabling Babel linker in a webcontainer environment via NG_BUILD_BABEL_LINKER=false', () => { + process.env['NG_BUILD_BABEL_LINKER'] = 'false'; + process.versions.webcontainer = '1.0.0'; + const { useBabelLinker } = loadEnvironmentOptions(); + + expect(useBabelLinker).toBeFalse(); + }); + + it('allows enabling Babel linker in a standard environment via NG_BUILD_BABEL_LINKER=true', () => { + process.env['NG_BUILD_BABEL_LINKER'] = 'true'; + delete process.versions.webcontainer; + const { useBabelLinker } = loadEnvironmentOptions(); + + expect(useBabelLinker).toBeTrue(); + }); +});