Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -54,6 +53,8 @@ let babelLinkerDeps:

let remapping: typeof import('@ampproject/remapping').default | undefined;

let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using a module-level variable oxcTransform that is assigned directly inside the try/catch block can lead to repeated failing import attempts. If the import of oxc-transform.js fails (which is expected in WebContainer environments when the WASM/WASI bindings fail), oxcTransform remains undefined. Consequently, every subsequent file transformation in the worker will re-attempt the import, throwing and catching errors repeatedly. This can severely degrade build performance, especially if the fallback script attempts synchronous process execution. We should instead use a Promise to cache the import attempt (including its failure state).

Suggested change
let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined;
let oxcTransformPromise: Promise<typeof import('../oxc/oxc-transform.js').transform | null> | undefined;


const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();

Expand Down Expand Up @@ -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;
}
}
Comment on lines +188 to +194

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Resolve the cached oxcTransformPromise and handle failures gracefully by returning null. This ensures that if the import fails once, subsequent file transformations will immediately receive null from the cached promise without re-attempting the import or throwing new exceptions.

  let transform: typeof import('../oxc/oxc-transform.js').transform | null = null;
  if (shouldRunOxc) {
    oxcTransformPromise ??= import('../oxc/oxc-transform.js')
      .then((m) => m.transform)
      .catch(() => null);
    transform = await oxcTransformPromise;
    if (!transform) {
      oxcLink = false;
    }
  }


if (shouldLink && (useBabelLinker || !oxcLink)) {
babelLinkerDeps ??= Promise.all([
import('@angular/compiler-cli/linker/babel'),
import('@angular/compiler-cli'),
Expand Down Expand Up @@ -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, {
Comment on lines +237 to +243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update the check and invocation to use the locally resolved transform function instead of the module-level oxcTransform variable.

Suggested change
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, {
if (transform && (oxcLink || advancedOptimizations)) {
const sideEffectFree = options.sideEffects === false;
const safeAngularPackage =
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
const topLevelSafeMode = !safeAngularPackage;
const result = transform(filename, code, {

link: oxcLink,
jit,
advancedOptimizations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
7 changes: 5 additions & 2 deletions packages/angular/build/src/utils/environment-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions packages/angular/build/src/utils/environment-options_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading