diff --git a/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts
new file mode 100644
index 000000000000..af28ab66b264
--- /dev/null
+++ b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts
@@ -0,0 +1,1001 @@
+/**
+ * @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.dev/license
+ */
+
+import { transform } from './oxc-transform';
+
+const NO_CHANGE = Symbol('NO_CHANGE');
+
+function cleanCode(code: string): string {
+ return code.replace(/\s+/g, '').replace(/,([}\])])/g, '$1');
+}
+
+function testCase({
+ input,
+ expected,
+ options,
+}: {
+ input: string;
+ expected: string | typeof NO_CHANGE;
+ options?: { wrapDecorators?: boolean };
+}): jasmine.ImplementationCallback {
+ return async () => {
+ const result = transform('test.js', input, {
+ sourcemap: false,
+ sideEffects: options?.wrapDecorators ? false : true,
+ pureAnnotate: false,
+ });
+ if (!result?.code) {
+ fail('Expected oxc-transform to return a transform result.');
+ } else {
+ const actualClean = cleanCode(result.code);
+ const expectedClean = cleanCode(expected === NO_CHANGE ? input : expected);
+ expect(actualClean).toEqual(expectedClean);
+ }
+ };
+}
+
+describe('adjust-static-class-members oxc-transform implementation', () => {
+ it(
+ 'elides empty ctorParameters function expression static field',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.ctorParameters = function () { return []; };
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'elides non-empty ctorParameters function expression static field',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.ctorParameters = function () { return [{type: Injector}]; };
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'elides empty ctorParameters arrow expression static field',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.ctorParameters = () => [];
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'elides non-empty ctorParameters arrow expression static field',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.ctorParameters = () => [{type: Injector}];
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'keeps ctorParameters static field without arrow/function expression',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.ctorParameters = 42;
+ `,
+ expected: `
+ export let SomeClass = /*#__PURE__*/ (() => {
+ class SomeClass {}
+ SomeClass.ctorParameters = 42;
+ return SomeClass;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides empty decorators static field with array literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.decorators = [];
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'elides non-empty decorators static field with array literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.decorators = [{ type: Injectable }];
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'keeps decorators static field without array literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.decorators = 42;
+ `,
+ expected: `
+ export let SomeClass = /*#__PURE__*/ (() => {
+ class SomeClass {}
+ SomeClass.decorators = 42;
+ return SomeClass;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides empty propDecorators static field with object literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.propDecorators = {};
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'elides non-empty propDecorators static field with object literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.propDecorators = { 'ngIf': [{ type: Input }] };
+ `,
+ expected: 'export class SomeClass {}',
+ }),
+ );
+
+ it(
+ 'keeps propDecorators static field without object literal',
+ testCase({
+ input: `
+ export class SomeClass {}
+ SomeClass.propDecorators = 42;
+ `,
+ expected: `
+ export let SomeClass = /*#__PURE__*/ (() => {
+ class SomeClass {}
+ SomeClass.propDecorators = 42;
+ return SomeClass;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap default exported class with no connected siblings',
+ testCase({
+ // NOTE: This could technically have no changes but the default export splitting detection
+ // does not perform class property analysis currently.
+ input: `
+ export default class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ export { CustomComponentEffects as default };
+ `,
+ }),
+ );
+
+ it(
+ 'does wrap not default exported class with only side effect fields',
+ testCase({
+ input: `
+ export default class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = console.log('foo');
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only side effect fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = console.log('foo');
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only side effect native fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ static someFieldWithSideEffects = console.log('foo');
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only instance native fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ someFieldWithSideEffects = console.log('foo');
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'wraps class with pure annotated side effect fields (#__PURE__)',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /*#__PURE__*/ console.log('foo');
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /*#__PURE__*/ console.log('foo');
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with pure annotated side effect native fields (#__PURE__)',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ static someFieldWithSideEffects = /*#__PURE__*/ console.log('foo');
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ static someFieldWithSideEffects = /*#__PURE__*/ console.log('foo');
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with pure annotated side effect fields (@__PURE__)',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /*@__PURE__*/ console.log('foo');
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /*@__PURE__*/ console.log('foo');
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with pure annotated side effect fields (@pureOrBreakMyCode)',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /**@pureOrBreakMyCode*/ console.log('foo');
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects =
+ /**@pureOrBreakMyCode*/ console.log('foo');
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with closure pure annotated side effect fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = /* @pureOrBreakMyCode */ console.log('foo');
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects =
+ /* @pureOrBreakMyCode */ console.log('foo');
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps exported class with a pure static field',
+ testCase({
+ input: `
+ export class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ `,
+ expected: `
+ export let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps exported class with a pure native static field',
+ testCase({
+ input: `
+ export class CustomComponentEffects {
+ static someField = 42;
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: `
+ export let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ static someField = 42;
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with a basic literal static field',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ return CustomComponentEffects;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with a pure static field',
+ testCase({
+ input: `
+ const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef;
+ const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__;
+ class TemplateRef {}
+ TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY;
+ `,
+ expected: `
+ const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef;
+ const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__;
+ let TemplateRef = /*#__PURE__*/ (() => {
+ class TemplateRef {}
+ TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY;
+ return TemplateRef;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with multiple pure static field',
+ testCase({
+ input: `
+ const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef;
+ const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__;
+ class TemplateRef {}
+ TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY;
+ TemplateRef.someField = 42;
+ `,
+ expected: `
+ const SWITCH_TEMPLATE_REF_FACTORY__POST_R3__ = injectTemplateRef;
+ const SWITCH_TEMPLATE_REF_FACTORY = SWITCH_TEMPLATE_REF_FACTORY__POST_R3__;
+ let TemplateRef = /*#__PURE__*/ (() => {
+ class TemplateRef {}
+ TemplateRef.__NG_ELEMENT_ID__ = SWITCH_TEMPLATE_REF_FACTORY;
+ TemplateRef.someField = 42;
+ return TemplateRef;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only some pure static fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ CustomComponentEffects.someFieldWithSideEffects = console.log('foo');
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only pure native static fields and some side effect static fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ static someField = 42;
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someFieldWithSideEffects = console.log('foo');
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with only some pure native static fields',
+ testCase({
+ input: `
+ class CustomComponentEffects {
+ static someField = 42;
+ static someFieldWithSideEffects = console.log('foo');
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap class with class decorators when wrapDecorators is false',
+ testCase({
+ input: `
+ let SomeClass = class SomeClass {
+ };
+ SomeClass = __decorate([
+ Dec()
+ ], SomeClass);
+ `,
+ expected: NO_CHANGE,
+ options: { wrapDecorators: false },
+ }),
+ );
+
+ it(
+ 'wraps class with Angular ɵfac static field (esbuild)',
+ testCase({
+ input: `
+ var Comp2Component = class _Comp2Component {
+ static {
+ this.ɵfac = function Comp2Component_Factory(t) {
+ return new (t || _Comp2Component)();
+ };
+ }
+ };
+ `,
+ expected: `
+ var Comp2Component = /*#__PURE__*/ (() => {
+ let Comp2Component = class _Comp2Component {
+ static {
+ this.ɵfac = function Comp2Component_Factory(t) {
+ return new (t || _Comp2Component)();
+ };
+ }
+ };
+ return Comp2Component;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with class decorators when wrapDecorators is true (esbuild output)',
+ testCase({
+ input: `
+ var ExampleClass = class {
+ method() {
+ }
+ };
+ __decorate([
+ SomeDecorator()
+ ], ExampleClass.prototype, "method", null);
+ `,
+ expected: `
+ var ExampleClass = /*#__PURE__*/ (() => {
+ let ExampleClass = class {
+ method() {}
+ };
+ __decorate([SomeDecorator()], ExampleClass.prototype, "method", null);
+ return ExampleClass;
+ })();
+ `,
+ options: { wrapDecorators: true },
+ }),
+ );
+
+ it(
+ 'wraps class with class decorators when wrapDecorators is true',
+ testCase({
+ input: `
+ let SomeClass = class SomeClass {
+ };
+ SomeClass = __decorate([
+ SomeDecorator()
+ ], SomeClass);
+ `,
+ expected: `
+ let SomeClass = /*#__PURE__*/ (() => {
+ let SomeClass = class SomeClass {
+ };
+ SomeClass = __decorate([
+ SomeDecorator()
+ ], SomeClass);
+ return SomeClass;
+ })();
+ `,
+ options: { wrapDecorators: true },
+ }),
+ );
+
+ it(
+ 'does not wrap class with constructor decorators when wrapDecorators is false',
+ testCase({
+ input: `
+ let SomeClass = class SomeClass {
+ constructor(foo) { }
+ };
+ SomeClass = __decorate([
+ __param(0, SomeDecorator)
+ ], SomeClass);
+ `,
+ expected: NO_CHANGE,
+ options: { wrapDecorators: false },
+ }),
+ );
+
+ it(
+ 'wraps class with constructor decorators when wrapDecorators is true',
+ testCase({
+ input: `
+ let SomeClass = class SomeClass {
+ constructor(foo) { }
+ };
+ SomeClass = __decorate([
+ __param(0, SomeDecorator)
+ ], SomeClass);
+ `,
+ expected: `
+ let SomeClass = /*#__PURE__*/ (() => {
+ let SomeClass = class SomeClass {
+ constructor(foo) { }
+ };
+ SomeClass = __decorate([
+ __param(0, SomeDecorator)
+ ], SomeClass);
+ return SomeClass;
+ })();
+ `,
+ options: { wrapDecorators: true },
+ }),
+ );
+
+ it(
+ 'does not wrap class with field decorators when wrapDecorators is false',
+ testCase({
+ input: `
+ class SomeClass {
+ constructor() {
+ this.foo = 42;
+ }
+ }
+ __decorate([
+ SomeDecorator
+ ], SomeClass.prototype, "foo", void 0);
+ `,
+ expected: NO_CHANGE,
+ options: { wrapDecorators: false },
+ }),
+ );
+
+ it(
+ 'wraps class with field decorators when wrapDecorators is true',
+ testCase({
+ input: `
+ class SomeClass {
+ constructor() {
+ this.foo = 42;
+ }
+ }
+ __decorate([
+ SomeDecorator
+ ], SomeClass.prototype, "foo", void 0);
+ `,
+ expected: `
+ let SomeClass = /*#__PURE__*/ (() => {
+ class SomeClass {
+ constructor() {
+ this.foo = 42;
+ }
+ }
+ __decorate([
+ SomeDecorator
+ ], SomeClass.prototype, "foo", void 0);
+ return SomeClass;
+ })();
+ `,
+ options: { wrapDecorators: true },
+ }),
+ );
+
+ it(
+ 'wraps class with Angular ɵfac static field',
+ testCase({
+ input: `
+ class CommonModule {
+ }
+ CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); };
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ }
+ CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); };
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with Angular ɵfac static block (ES2022 + useDefineForClassFields: false)',
+ testCase({
+ input: `
+ class CommonModule {
+ static { this.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); }; }
+ static { this.ɵmod = ɵngcc0.ɵɵdefineNgModule({ type: CommonModule }); }
+ }
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ static {
+ this.ɵfac = function CommonModule_Factory(t) {
+ return new (t || CommonModule)();
+ };
+ }
+ static {
+ this.ɵmod = ɵngcc0.ɵɵdefineNgModule({
+ type: CommonModule,
+ });
+ }
+ }
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap class with side effect full static block (ES2022 + useDefineForClassFields: false)',
+ testCase({
+ input: `
+ class CommonModule {
+ static { globalThis.bar = 1 }
+ }
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'wraps class with Angular ɵmod static field',
+ testCase({
+ input: `
+ class CommonModule {
+ }
+ CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule });
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ }
+ CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule });
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with Angular ɵinj static field',
+ testCase({
+ input: `
+ class CommonModule {
+ }
+ CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [
+ { provide: NgLocalization, useClass: NgLocaleLocalization },
+ ] });
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ }
+ CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [
+ {
+ provide: NgLocalization,
+ useClass: NgLocaleLocalization
+ },
+ ] });
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with multiple Angular static fields',
+ testCase({
+ input: `
+ class CommonModule {
+ }
+ CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); };
+ CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule });
+ CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [
+ { provide: NgLocalization, useClass: NgLocaleLocalization },
+ ] });
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ }
+ CommonModule.ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); };
+ CommonModule.ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule });
+ CommonModule.ɵinj = /*@__PURE__*/ ɵngcc0.ɵɵdefineInjector({ providers: [
+ {
+ provide: NgLocalization,
+ useClass: NgLocaleLocalization
+ },
+ ]});
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps class with multiple Angular native static fields',
+ testCase({
+ input: `
+ class CommonModule {
+ static ɵfac = function CommonModule_Factory(t) { return new (t || CommonModule)(); };
+ static ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({ type: CommonModule });
+ static ɵinj = ɵngcc0.ɵɵdefineInjector({ providers: [
+ { provide: NgLocalization, useClass: NgLocaleLocalization },
+ ] });
+ }
+ `,
+ expected: `
+ let CommonModule = /*#__PURE__*/ (() => {
+ class CommonModule {
+ static ɵfac = function CommonModule_Factory(t) {
+ return new (t || CommonModule)();
+ };
+ static ɵmod = /*@__PURE__*/ ɵngcc0.ɵɵdefineNgModule({
+ type: CommonModule,
+ });
+ static ɵinj = ɵngcc0.ɵɵdefineInjector({
+ providers: [
+ {
+ provide: NgLocalization,
+ useClass: NgLocaleLocalization,
+ },
+ ],
+ });
+ }
+ return CommonModule;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'wraps default exported class with pure static fields',
+ testCase({
+ input: `
+ export default class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ `,
+ expected: `
+ let CustomComponentEffects = /*#__PURE__*/ (() => {
+ class CustomComponentEffects {
+ constructor(_actions) {
+ this._actions = _actions;
+ this.doThis = this._actions;
+ }
+ }
+ CustomComponentEffects.someField = 42;
+ return CustomComponentEffects;
+ })();
+ export { CustomComponentEffects as default };
+ `,
+ }),
+ );
+ it(
+ 'supports class with empty static block and wraps pure static properties',
+ testCase({
+ input: `
+ class MyClass {
+ static {}
+ static ɵprov = 42;
+ }
+ `,
+ expected: `
+ let MyClass = /*#__PURE__*/ (() => {
+ class MyClass {
+ static {}
+ static ɵprov = 42;
+ }
+ return MyClass;
+ })();
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap class with non-empty static block',
+ testCase({
+ input: `
+ class MyClass {
+ static { console.log(1); }
+ static ɵprov = 42;
+ }
+ `,
+ expected: `
+ class MyClass {
+ static { console.log(1); }
+ static ɵprov = 42;
+ }
+ `,
+ }),
+ );
+});
diff --git a/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts
new file mode 100644
index 000000000000..fc01eecf991a
--- /dev/null
+++ b/packages/angular/build/src/tools/babel/plugins/adjust-typescript-enums_oxc_spec.ts
@@ -0,0 +1,283 @@
+/**
+ * @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.dev/license
+ */
+
+import { format } from 'prettier';
+import { transform } from './oxc-transform';
+
+const NO_CHANGE = Symbol('NO_CHANGE');
+
+function testCase({
+ input,
+ expected,
+}: {
+ input: string;
+ expected: string | typeof NO_CHANGE;
+}): jasmine.ImplementationCallback {
+ return async () => {
+ const result = transform('test.js', input, {
+ sourcemap: false,
+ pureAnnotate: false,
+ });
+ if (!result?.code) {
+ fail('Expected oxc-transform to return a transform result.');
+ } else {
+ expect(await format(result.code, { parser: 'babel' })).toEqual(
+ await format(expected === NO_CHANGE ? input : expected, { parser: 'babel' }),
+ );
+ }
+ };
+}
+
+describe('adjust-typescript-enums oxc-transform implementation', () => {
+ it(
+ 'wraps unexported TypeScript enums',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
+ `,
+ expected: `
+ var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || {});
+ `,
+ }),
+ );
+
+ it(
+ 'wraps exported TypeScript enums',
+ testCase({
+ input: `
+ export var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
+ `,
+ expected: `
+ export var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || {});
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap exported TypeScript enums from CommonJS (<5.1)',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy = exports.ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = {}));
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'wraps exported TypeScript enums from CommonJS (5.1+)',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = ChangeDetectionStrategy = {}));
+ `,
+ expected: `
+ var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || (exports.ChangeDetectionStrategy = ChangeDetectionStrategy = {}));
+ `,
+ }),
+ );
+
+ it(
+ 'wraps TypeScript enums with custom numbering',
+ testCase({
+ input: `
+ export var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 5] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 8] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
+ `,
+ expected: `
+ export var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 5)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 8)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || {});
+ `,
+ }),
+ );
+
+ it(
+ 'wraps string-based TypeScript enums',
+ testCase({
+ input: `
+ var NotificationKind;
+ (function (NotificationKind) {
+ NotificationKind["NEXT"] = "N";
+ NotificationKind["ERROR"] = "E";
+ NotificationKind["COMPLETE"] = "C";
+ })(NotificationKind || (NotificationKind = {}));
+ `,
+ expected: `
+ var NotificationKind = /*#__PURE__*/ (function (NotificationKind) {
+ NotificationKind["NEXT"] = "N";
+ NotificationKind["ERROR"] = "E";
+ NotificationKind["COMPLETE"] = "C";
+ return NotificationKind;
+ })(NotificationKind || {});
+ `,
+ }),
+ );
+
+ it(
+ 'wraps enums that were renamed due to scope hoisting',
+ testCase({
+ input: `
+ var NotificationKind$1;
+ (function (NotificationKind) {
+ NotificationKind["NEXT"] = "N";
+ NotificationKind["ERROR"] = "E";
+ NotificationKind["COMPLETE"] = "C";
+ })(NotificationKind$1 || (NotificationKind$1 = {}));
+ `,
+ expected: `
+ var NotificationKind$1 = /*#__PURE__*/ (function (NotificationKind) {
+ NotificationKind["NEXT"] = "N";
+ NotificationKind["ERROR"] = "E";
+ NotificationKind["COMPLETE"] = "C";
+ return NotificationKind;
+ })(NotificationKind$1 || {});
+ `,
+ }),
+ );
+
+ it(
+ 'maintains multi-line comments',
+ testCase({
+ input: `
+ /**
+ * Supported http methods.
+ * @deprecated use @angular/common/http instead
+ */
+ var RequestMethod;
+ (function (RequestMethod) {
+ RequestMethod[RequestMethod["Get"] = 0] = "Get";
+ RequestMethod[RequestMethod["Post"] = 1] = "Post";
+ RequestMethod[RequestMethod["Put"] = 2] = "Put";
+ RequestMethod[RequestMethod["Delete"] = 3] = "Delete";
+ RequestMethod[RequestMethod["Options"] = 4] = "Options";
+ RequestMethod[RequestMethod["Head"] = 5] = "Head";
+ RequestMethod[RequestMethod["Patch"] = 6] = "Patch";
+ })(RequestMethod || (RequestMethod = {}));
+ `,
+ expected: `
+ /**
+ * Supported http methods.
+ * @deprecated use @angular/common/http instead
+ */
+ var RequestMethod = /*#__PURE__*/ (function (RequestMethod) {
+ RequestMethod[(RequestMethod["Get"] = 0)] = "Get";
+ RequestMethod[(RequestMethod["Post"] = 1)] = "Post";
+ RequestMethod[(RequestMethod["Put"] = 2)] = "Put";
+ RequestMethod[(RequestMethod["Delete"] = 3)] = "Delete";
+ RequestMethod[(RequestMethod["Options"] = 4)] = "Options";
+ RequestMethod[(RequestMethod["Head"] = 5)] = "Head";
+ RequestMethod[(RequestMethod["Patch"] = 6)] = "Patch";
+ return RequestMethod;
+ })(RequestMethod || {});
+ `,
+ }),
+ );
+
+ it(
+ 'does not wrap TypeScript enums with side effect values',
+ testCase({
+ input: `
+ export var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = console.log('foo');
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'does not wrap object literals similar to TypeScript enums',
+ testCase({
+ input: `
+ const RendererStyleFlags3 = {
+ Important: 1,
+ DashCase: 2,
+ };
+ if (typeof RendererStyleFlags3 === 'object') {
+ RendererStyleFlags3[RendererStyleFlags3.Important] = 'DashCase';
+ }
+ RendererStyleFlags3[RendererStyleFlags3.Important] = 'Important';
+ `,
+ expected: NO_CHANGE,
+ }),
+ );
+
+ it(
+ 'wraps TypeScript enums',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy || (ChangeDetectionStrategy = {}));
+ `,
+ expected: `
+ var ChangeDetectionStrategy = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["OnPush"] = 0)] = "OnPush";
+ ChangeDetectionStrategy[(ChangeDetectionStrategy["Default"] = 1)] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy || {});
+ `,
+ }),
+ );
+
+ it(
+ 'should wrap TypeScript enums if the declaration identifier has been renamed to avoid collisions',
+ testCase({
+ input: `
+ var ChangeDetectionStrategy$1;
+ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ })(ChangeDetectionStrategy$1 || (ChangeDetectionStrategy$1 = {}));
+ `,
+ expected: `
+ var ChangeDetectionStrategy$1 = /*#__PURE__*/ (function (ChangeDetectionStrategy) {
+ ChangeDetectionStrategy[ChangeDetectionStrategy["OnPush"] = 0] = "OnPush";
+ ChangeDetectionStrategy[ChangeDetectionStrategy["Default"] = 1] = "Default";
+ return ChangeDetectionStrategy;
+ })(ChangeDetectionStrategy$1 || {});
+ `,
+ }),
+ );
+});
diff --git a/packages/angular/build/src/tools/babel/plugins/elide-angular-metadata_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/elide-angular-metadata_oxc_spec.ts
new file mode 100644
index 000000000000..f4d6c09b6c19
--- /dev/null
+++ b/packages/angular/build/src/tools/babel/plugins/elide-angular-metadata_oxc_spec.ts
@@ -0,0 +1,213 @@
+/**
+ * @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.dev/license
+ */
+
+import { transform } from './oxc-transform';
+
+function cleanCode(code: string): string {
+ return code
+ .replace(/\s+/g, '')
+ .replace(/"/g, "'")
+ .replace(/;}/g, '}')
+ .replace(/,([}\])])/g, '$1');
+}
+
+function testCase({
+ input,
+ expected,
+}: {
+ input: string;
+ expected: string;
+}): jasmine.ImplementationCallback {
+ return async () => {
+ const result = transform('test.js', input, {
+ sourcemap: false,
+ pureAnnotate: false,
+ });
+ if (!result?.code) {
+ fail('Expected oxc-transform to return a transform result.');
+ } else {
+ const actualClean = cleanCode(result.code);
+ const expectedClean = cleanCode(expected);
+ expect(actualClean).toEqual(expectedClean);
+ }
+ };
+}
+
+describe('elide-angular-metadata oxc-transform implementation', () => {
+ it(
+ 'elides pure annotated ɵsetClassMetadata',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(Clazz, [{
+ type: Component,
+ args: [{
+ selector: 'app-lazy',
+ template: 'very lazy',
+ styles: []
+ }]
+ }], null, null); })();
+ `,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (function () { void 0 })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides JIT mode protected ɵsetClassMetadata',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadata(SomeClass, [{
+ type: Component,
+ args: [{
+ selector: 'app-lazy',
+ template: 'very lazy',
+ styles: []
+ }]
+ }], null, null); })();`,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ (function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })();`,
+ }),
+ );
+
+ it(
+ 'elides ɵsetClassMetadata inside an arrow-function-based IIFE',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (() => { i0.ɵsetClassMetadata(Clazz, [{
+ type: Component,
+ args: [{
+ selector: 'app-lazy',
+ template: 'very lazy',
+ styles: []
+ }]
+ }], null, null); })();
+ `,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (() => { void 0 })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides pure annotated ɵsetClassMetadataAsync',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (function () {
+ i0.ɵsetClassMetadataAsync(SomeClass,
+ function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; },
+ function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{
+ type: Component,
+ args: [{
+ selector: 'test-cmp',
+ standalone: true,
+ imports: [CmpA, LocalDep],
+ template: '{#defer}{/defer}',
+ }]
+ }], null, null); });
+ })();
+ `,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (function () { void 0 })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides JIT mode protected ɵsetClassMetadataAsync',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ (function () {
+ (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵsetClassMetadataAsync(SomeClass,
+ function () { return [import("./cmp-a").then(function (m) { return m.CmpA; })]; },
+ function (CmpA) { i0.ɵsetClassMetadata(SomeClass, [{
+ type: Component,
+ args: [{
+ selector: 'test-cmp',
+ standalone: true,
+ imports: [CmpA, LocalDep],
+ template: '{#defer}{/defer}',
+ }]
+ }], null, null); });
+ })();
+ `,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ (function () { (typeof ngJitMode === "undefined" || ngJitMode) && void 0 })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides arrow-function-based ɵsetClassMetadataAsync',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (() => {
+ i0.ɵsetClassMetadataAsync(SomeClass,
+ () => [import("./cmp-a").then(m => m.CmpA)],
+ (CmpA) => { i0.ɵsetClassMetadata(SomeClass, [{
+ type: Component,
+ args: [{
+ selector: 'test-cmp',
+ standalone: true,
+ imports: [CmpA, LocalDep],
+ template: '{#defer}{/defer}',
+ }]
+ }], null, null); });
+ })();
+ `,
+ expected: `
+ import { Component } from '@angular/core';
+ export class SomeClass {}
+ /*@__PURE__*/ (() => { void 0 })();
+ `,
+ }),
+ );
+
+ it(
+ 'elides ɵsetClassDebugInfo',
+ testCase({
+ input: `
+ import { Component } from '@angular/core';
+ class SomeClass {}
+ (() => {
+ (typeof ngDevMode === 'undefined' || ngDevMode) &&
+ i0.ɵsetClassDebugInfo(SomeClass, { className: 'SomeClass' });
+ })();
+ `,
+ expected: `
+ import { Component } from "@angular/core";
+ class SomeClass {}
+ (() => {
+ (typeof ngDevMode === "undefined" || ngDevMode) && void 0;
+ })();
+ `,
+ }),
+ );
+});
diff --git a/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts
new file mode 100644
index 000000000000..2c69d98cdf1a
--- /dev/null
+++ b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts
@@ -0,0 +1,765 @@
+/**
+ * @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.dev/license
+ */
+
+import remapping, { type EncodedSourceMap } from '@ampproject/remapping';
+import type { BindingIdentifier, Class, Node } from '@oxc-project/types';
+import { MagicString } from 'magic-string';
+import { Visitor, parseSync } from 'oxc-parser';
+import { loadInputSourceMap } from '../../../utils/source-map';
+
+export interface OxcTransformOptions {
+ sourcemap?: boolean;
+ jit?: boolean;
+ sideEffects?: boolean;
+ topLevelSafeMode?: boolean;
+ pureAnnotate?: boolean;
+}
+
+/**
+ * A set of constructor names that are considered to be side-effect free.
+ */
+const sideEffectFreeConstructors = new Set(['InjectionToken']);
+
+/**
+ * A set of TypeScript helper function names used by the helper name matcher utility function.
+ */
+const tslibHelpers = new Set([
+ '__extends',
+ '__assign',
+ '__rest',
+ '__decorate',
+ '__param',
+ '__esDecorate',
+ '__runInitializers',
+ '__propKey',
+ '__setFunctionName',
+ '__metadata',
+ '__awaiter',
+ '__generator',
+ '__exportStar',
+ '__values',
+ '__read',
+ '__privateGet',
+ '__privateSet',
+ '__privateMethod',
+ '__addDisposableResource',
+ '__disposeResources',
+]);
+
+/**
+ * Determines whether an identifier name matches one of the TypeScript helper function names.
+ *
+ * @param name The identifier name to check.
+ * @returns True if the name matches a TypeScript helper name; otherwise, false.
+ */
+function isTslibHelperName(name: string): boolean {
+ const nameParts = name.split('$');
+ const originalName = nameParts[0];
+
+ if (nameParts.length > 2 || (nameParts.length === 2 && !/^\d+$/.test(nameParts[1]))) {
+ return false;
+ }
+
+ return tslibHelpers.has(originalName);
+}
+
+/**
+ * A set of Babel helper function names that are intended to cause side effects.
+ */
+const babelHelpers = new Set(['_defineProperty']);
+
+/**
+ * Determines whether an identifier name matches one of the Babel helper function names.
+ *
+ * @param name The identifier name to check.
+ * @returns True if the name matches a Babel helper name; otherwise, false.
+ */
+function isBabelHelperName(name: string): boolean {
+ return babelHelpers.has(name);
+}
+
+/**
+ * A set of Angular static properties that should be wrapped in pure IIFE statements.
+ */
+const angularStaticsToWrap = new Set([
+ 'ɵcmp',
+ 'ɵdir',
+ 'ɵfac',
+ 'ɵinj',
+ 'ɵmod',
+ 'ɵpipe',
+ 'ɵprov',
+ 'INJECTOR_KEY',
+]);
+
+/**
+ * A set of Angular metadata decorator functions that can be elided.
+ */
+const angularMetadataFunctions = new Set([
+ 'ɵsetClassMetadata',
+ 'ɵsetClassMetadataAsync',
+ 'ɵsetClassDebugInfo',
+]);
+
+/**
+ * A map of static properties and their matcher predicate functions to check if they
+ * can be safely elided from class declarations.
+ */
+const angularStaticsToElide: Record boolean> = {
+ 'ctorParameters'(node) {
+ return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
+ },
+ 'decorators'(node) {
+ return node.type === 'ArrayExpression';
+ },
+ 'propDecorators'(node) {
+ return node.type === 'ObjectExpression';
+ },
+};
+
+/**
+ * Determines whether an AST node is considered safe for pure evaluation (side-effect free).
+ *
+ * @param node The AST node to check.
+ * @returns True if the node is side-effect free; otherwise, false.
+ */
+function isPure(node: Node): boolean {
+ switch (node.type) {
+ case 'Identifier':
+ case 'Literal':
+ return true;
+ case 'BinaryExpression':
+ case 'LogicalExpression':
+ return isPure(node.left) && isPure(node.right);
+ case 'UnaryExpression':
+ return isPure(node.argument);
+ case 'MemberExpression':
+ return isPure(node.object) && (!node.computed || isPure(node.property));
+ case 'ObjectExpression':
+ return node.properties.every((p) => p.type === 'Property' && isPure(p.value));
+ case 'ArrayExpression':
+ return node.elements.every((e) => !e || isPure(e));
+ case 'ParenthesizedExpression':
+ return isPure(node.expression);
+ default:
+ return false;
+ }
+}
+
+/**
+ * Recursively unwraps any ParenthesizedExpression wrapper nodes to get the inner expression.
+ *
+ * @param node The potentially parenthesized AST node.
+ * @returns The inner non-parenthesized AST node.
+ */
+function unwrapParentheses(node: Node): Node {
+ while (node && node.type === 'ParenthesizedExpression') {
+ node = node.expression;
+ }
+
+ return node;
+}
+
+/**
+ * Determines whether a class static property assignment is safe to be wrapped in a pure IIFE.
+ *
+ * @param propertyName The name of the property.
+ * @param assignmentValue The AST node representing the value assigned.
+ * @param code The source code of the file.
+ * @returns True if the property can be wrapped safely; otherwise, false.
+ */
+function canWrapProperty(propertyName: string, assignmentValue: Node, code: string): boolean {
+ if (angularStaticsToWrap.has(propertyName)) {
+ return true;
+ }
+
+ const prefix = code.substring(Math.max(0, assignmentValue.start - 100), assignmentValue.start);
+ if (/(\/\*[\s\S]*?(?:@__PURE__|#__PURE__|@pureOrBreakMyCode)[\s\S]*?\*\/)\s*$/.test(prefix)) {
+ return true;
+ }
+
+ return isPure(assignmentValue);
+}
+
+/**
+ * Analyzes static properties inside a class body to determine if the class has any
+ * side-effecting static property initializers.
+ *
+ * @param classNode The Class AST node.
+ * @param code The source code of the file.
+ * @returns True if the class static properties are pure and can be wrapped; otherwise, false.
+ */
+function analyzeClassStaticProperties(classNode: Node, code: string): boolean {
+ let shouldWrap = false;
+ const body = (classNode as Class).body.body;
+ for (const element of body) {
+ if (element.type === 'PropertyDefinition') {
+ if (!element.static) {
+ continue;
+ }
+
+ const key = element.key;
+ const value = element.value;
+ if (key.type === 'Identifier' && (!value || canWrapProperty(key.name, value, code))) {
+ shouldWrap = true;
+ } else {
+ shouldWrap = false;
+ break;
+ }
+ } else if (element.type === 'StaticBlock') {
+ const blockBody = element.body;
+ if (blockBody.length === 0) {
+ continue;
+ }
+ if (blockBody.length > 1) {
+ shouldWrap = false;
+ break;
+ }
+ const expressionStatement = blockBody[0];
+ if (expressionStatement && expressionStatement.type === 'ExpressionStatement') {
+ const assignment = expressionStatement.expression;
+ if (
+ assignment &&
+ assignment.type === 'AssignmentExpression' &&
+ assignment.left.type === 'MemberExpression'
+ ) {
+ const left = assignment.left;
+ if (left.object.type === 'ThisExpression' && left.property.type === 'Identifier') {
+ if (canWrapProperty(left.property.name, assignment.right, code)) {
+ shouldWrap = true;
+ continue;
+ }
+ }
+ }
+ }
+ shouldWrap = false;
+ break;
+ }
+ }
+
+ return shouldWrap;
+}
+
+/**
+ * Executes a single-pass optimized transformation using oxc-parser and magic-string.
+ * Performs typescript enum wrapping, static class members elision/wrapping, angular metadata elision,
+ * and top-level pure function annotations.
+ *
+ * @param filename The absolute path of the file being transformed.
+ * @param code The string source content of the file.
+ * @param options Configuration options specifying which optimization steps to run.
+ * @returns The transformed code string and an optional source map.
+ */
+// eslint-disable-next-line max-lines-per-function
+export function transform(filename: string, code: string, options: OxcTransformOptions) {
+ const { program } = parseSync(filename, code, { range: true });
+ const s = new MagicString(code);
+
+ const sideEffectFree = options.sideEffects === false;
+ const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
+ const topLevelSafeMode = options.topLevelSafeMode ?? false;
+ const wrapDecorators = sideEffectFree;
+ const pureAnnotate = options.pureAnnotate ?? true;
+
+ /**
+ * Scans backwards from the specified start index to check if a pure comment (e.g. `/*@__PURE__*\/`)
+ * already precedes the node.
+ *
+ * @param start The index where the node starts.
+ * @returns True if a pure comment is already present; otherwise, false.
+ */
+ function hasPureComment(start: number): boolean {
+ let i = start - 1;
+ while (i >= 0 && /\s/.test(code[i])) {
+ i--;
+ }
+ if (i < 1 || code[i] !== '/' || code[i - 1] !== '*') {
+ return false;
+ }
+ const commentEnd = i + 1;
+ const commentStart = code.lastIndexOf('/*', commentEnd);
+ if (commentStart === -1) {
+ return false;
+ }
+ const commentContent = code.substring(commentStart + 2, commentEnd - 2);
+
+ return commentContent.includes('@__PURE__') || commentContent.includes('#__PURE__');
+ }
+
+ const editedRanges: { start: number; end: number }[] = [];
+
+ /**
+ * Records a range in the source code that has been modified, preventing subsequent nested mutations.
+ *
+ * @param start The start index of the modified range.
+ * @param end The end index of the modified range.
+ */
+ function markEdited(start: number, end: number) {
+ editedRanges.push({ start, end });
+ }
+
+ /**
+ * Checks if the specified range falls inside an already modified section of code.
+ *
+ * @param start The start index of the range.
+ * @param end The end index of the range.
+ * @returns True if the range is already edited; otherwise, false.
+ */
+ function isAlreadyEdited(start: number, end: number): boolean {
+ return editedRanges.some((r) => start >= r.start && end <= r.end);
+ }
+
+ // Track function nesting depth and closest function expression wrapper
+ let functionDepth = 0;
+ let classDepth = 0;
+ const functionStack: Node[] = [];
+
+ /**
+ * Scans and rewrites TypeScript emitted enum declarations in the statement block.
+ * Wraps enum statements inside a pure IIFE assignable directly to the enum variable.
+ *
+ * @param body The array of statement AST nodes to process.
+ */
+ function adjustTypeScriptEnumsInStatements(body: Node[]) {
+ for (let i = 0; i < body.length - 1; i++) {
+ const statement = body[i];
+ let declStatement = statement;
+ if (
+ statement.type === 'ExportNamedDeclaration' &&
+ statement.declaration?.type === 'VariableDeclaration'
+ ) {
+ declStatement = statement.declaration;
+ }
+
+ if (
+ declStatement.type !== 'VariableDeclaration' ||
+ declStatement.kind !== 'var' ||
+ declStatement.declarations.length !== 1
+ ) {
+ continue;
+ }
+ const decl = declStatement.declarations[0];
+ if (decl.init || decl.id.type !== 'Identifier') {
+ continue;
+ }
+
+ const nextStatement = body[i + 1];
+ if (nextStatement.type !== 'ExpressionStatement') {
+ continue;
+ }
+
+ const nextExpr = unwrapParentheses(nextStatement.expression);
+ if (nextExpr.type !== 'CallExpression' || nextExpr.arguments.length !== 1) {
+ continue;
+ }
+
+ const arg = unwrapParentheses(nextExpr.arguments[0]);
+ if (arg.type !== 'LogicalExpression' || arg.operator !== '||') {
+ continue;
+ }
+
+ const argLeft = unwrapParentheses(arg.left);
+ if (argLeft.type !== 'Identifier' || argLeft.name !== decl.id.name) {
+ continue;
+ }
+
+ const rightCallArgument = unwrapParentheses(arg.right);
+ if (rightCallArgument.type !== 'AssignmentExpression') {
+ continue;
+ }
+
+ const callee = unwrapParentheses(nextExpr.callee);
+ if (
+ callee.type !== 'FunctionExpression' ||
+ callee.params.length !== 1 ||
+ !callee.body ||
+ callee.body.type !== 'BlockStatement'
+ ) {
+ continue;
+ }
+
+ const param = callee.params[0];
+ if (param.type !== 'Identifier') {
+ continue;
+ }
+ const paramName = (param as BindingIdentifier).name;
+
+ // Check if all statements in body are pure assignments
+ let hasElements = false;
+ let allPure = true;
+ for (const enumStatement of callee.body.body) {
+ if (enumStatement.type !== 'ExpressionStatement') {
+ allPure = false;
+ break;
+ }
+
+ const enumValueAssignment = unwrapParentheses(enumStatement.expression);
+ if (
+ enumValueAssignment.type !== 'AssignmentExpression' ||
+ !isPure(enumValueAssignment.right)
+ ) {
+ allPure = false;
+ break;
+ }
+
+ hasElements = true;
+ }
+
+ if (!allPure || !hasElements) {
+ continue;
+ }
+
+ // 1. Remove only the trailing characters/semicolon of the expression statement
+ s.remove(nextExpr.end, nextStatement.end);
+ markEdited(nextExpr.end, nextStatement.end);
+
+ // 2. Add return statement inside IIFE body
+ s.appendRight(callee.body.end - 1, `; return ${paramName};`);
+
+ // 3. Remove `Name = ` assignment in arguments if it's a simple identifier
+ if (rightCallArgument.left.type === 'Identifier') {
+ s.overwrite(
+ arg.right.start,
+ arg.right.end,
+ code.substring(rightCallArgument.right.start, rightCallArgument.right.end),
+ );
+ markEdited(arg.right.start, arg.right.end);
+ }
+
+ // 4. Move IIFE to the var initializer
+ s.move(nextExpr.start, nextExpr.end, decl.id.end);
+ s.appendLeft(decl.id.end, ' = /*#__PURE__*/ ');
+ markEdited(nextExpr.start, nextExpr.end);
+ }
+ }
+
+ /**
+ * Scans and rewrites static class member initializers in the statement block.
+ * Groups externalized class static assignments into pure wrappers or elides them when safe.
+ *
+ * @param body The array of statement AST nodes to process.
+ */
+ function adjustStaticMembersInStatements(body: Node[]) {
+ for (let i = 0; i < body.length; i++) {
+ const statement = body[i];
+ let classNode: Node | null = null;
+ let isExportDefault = false;
+ let isExportNamed = false;
+ let isVariableClass = false;
+ let classIdName = '';
+ if (statement.type === 'ClassDeclaration') {
+ classNode = statement;
+ classIdName = classNode.id?.name || '';
+ } else if (
+ statement.type === 'ExportNamedDeclaration' &&
+ statement.declaration?.type === 'ClassDeclaration'
+ ) {
+ classNode = statement.declaration;
+ classIdName = classNode.id?.name || '';
+ isExportNamed = true;
+ } else if (
+ statement.type === 'ExportDefaultDeclaration' &&
+ statement.declaration?.type === 'ClassDeclaration'
+ ) {
+ classNode = statement.declaration;
+ classIdName = classNode.id?.name || '';
+ isExportDefault = true;
+ } else if (statement.type === 'VariableDeclaration' && statement.declarations.length === 1) {
+ const decl = statement.declarations[0];
+ if (decl.init && decl.init.type === 'ClassExpression' && decl.id.type === 'Identifier') {
+ classNode = decl.init;
+ classIdName = decl.id.name;
+ isVariableClass = true;
+ }
+ }
+
+ if (!classNode || !classIdName) {
+ continue;
+ }
+
+ const wrapStatementPaths: { statement: Node; type: 'wrap' | 'decorate' | 'elide' }[] = [];
+ let hasPotentialSideEffects = false;
+
+ for (let j = i + 1; j < body.length; j++) {
+ const nextStatement = body[j];
+ if (nextStatement.type !== 'ExpressionStatement') {
+ break;
+ }
+
+ const nextExpression = nextStatement.expression;
+
+ // Case 1: __decorate(...)
+ if (nextExpression.type === 'CallExpression') {
+ if (
+ nextExpression.callee.type !== 'Identifier' ||
+ nextExpression.callee.name !== '__decorate'
+ ) {
+ break;
+ }
+
+ if (wrapDecorators) {
+ wrapStatementPaths.push({ statement: nextStatement, type: 'decorate' });
+ } else {
+ hasPotentialSideEffects = true;
+ }
+ continue;
+ }
+
+ // Case 2: AssignmentExpression
+ if (nextExpression.type !== 'AssignmentExpression') {
+ break;
+ }
+
+ const left = nextExpression.left;
+
+ if (left.type === 'Identifier') {
+ if (
+ left.name !== classIdName ||
+ nextExpression.right.type !== 'CallExpression' ||
+ nextExpression.right.callee.type !== 'Identifier' ||
+ nextExpression.right.callee.name !== '__decorate'
+ ) {
+ break;
+ }
+
+ if (wrapDecorators) {
+ wrapStatementPaths.push({ statement: nextStatement, type: 'decorate' });
+ } else {
+ hasPotentialSideEffects = true;
+ }
+ continue;
+ }
+
+ if (
+ left.type !== 'MemberExpression' ||
+ left.object.type !== 'Identifier' ||
+ left.object.name !== classIdName ||
+ left.property.type !== 'Identifier'
+ ) {
+ break;
+ }
+
+ const propertyName = left.property.name;
+ const assignmentValue = nextExpression.right;
+
+ if (angularStaticsToElide[propertyName]?.(assignmentValue)) {
+ wrapStatementPaths.push({ statement: nextStatement, type: 'elide' });
+ } else if (canWrapProperty(propertyName, assignmentValue, code)) {
+ wrapStatementPaths.push({ statement: nextStatement, type: 'wrap' });
+ } else {
+ hasPotentialSideEffects = true;
+ }
+ }
+
+ // Check class body static properties
+ const shouldWrapClassStaticProperties = analyzeClassStaticProperties(classNode, code);
+
+ // Perform elisions immediately
+ for (const item of wrapStatementPaths) {
+ if (item.type === 'elide') {
+ s.remove(item.statement.start, item.statement.end);
+ markEdited(item.statement.start, item.statement.end);
+ }
+ }
+
+ const activeWrapPaths = wrapStatementPaths.filter(
+ (p) => p.type === 'wrap' || p.type === 'decorate',
+ );
+
+ if (
+ !hasPotentialSideEffects &&
+ (activeWrapPaths.length > 0 || shouldWrapClassStaticProperties)
+ ) {
+ const lastStatement =
+ activeWrapPaths.length > 0
+ ? activeWrapPaths[activeWrapPaths.length - 1].statement
+ : classNode;
+
+ if (isExportDefault) {
+ // 1. Remove `export default `
+ s.overwrite(statement.start, classNode.start, '');
+ // 2. Wrap in IIFE
+ s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendRight(
+ lastStatement.end,
+ `\nreturn ${classIdName};\n})();\nexport { ${classIdName} as default };`,
+ );
+ } else if (isExportNamed) {
+ // 1. Export is kept, turn `class` into `let ClassName = IIFE`
+ s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`);
+ } else if (isVariableClass) {
+ // Wrap class inside init: `/*#__PURE__*/ (() => { let ClassName = class ClassName {}; return ClassName; })()`
+ s.appendLeft(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `);
+ const terminator = activeWrapPaths.length === 0 ? ';' : '';
+ const iifeClosing = activeWrapPaths.length === 0 ? '})()' : '})();';
+ s.appendRight(lastStatement.end, `${terminator}\nreturn ${classIdName};\n${iifeClosing}`);
+ } else {
+ // Standard ClassDeclaration
+ s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
+ s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`);
+ }
+
+ markEdited(statement.start, lastStatement.end);
+
+ // Fast-forward outer loop index to skip the statements we wrapped
+ i += wrapStatementPaths.length;
+ } else if (isExportDefault && !hasPotentialSideEffects) {
+ // Splitting default export even when not wrapped
+ s.overwrite(statement.start, classNode.start, '');
+ s.appendRight(classNode.end, `\nexport { ${classIdName} as default };`);
+ markEdited(statement.start, classNode.end);
+ }
+ }
+ }
+
+ const visitor = new Visitor({
+ ClassDeclaration(node) {
+ classDepth++;
+ },
+ 'ClassDeclaration:exit'() {
+ classDepth--;
+ },
+ ClassExpression(node) {
+ classDepth++;
+ },
+ 'ClassExpression:exit'() {
+ classDepth--;
+ },
+ FunctionDeclaration(node) {
+ functionDepth++;
+ functionStack.push(node);
+ },
+ 'FunctionDeclaration:exit'() {
+ functionDepth--;
+ functionStack.pop();
+ },
+ FunctionExpression(node) {
+ functionDepth++;
+ functionStack.push(node);
+ },
+ 'FunctionExpression:exit'() {
+ functionDepth--;
+ functionStack.pop();
+ },
+ ArrowFunctionExpression(node) {
+ functionDepth++;
+ functionStack.push(node);
+ },
+ 'ArrowFunctionExpression:exit'() {
+ functionDepth--;
+ functionStack.pop();
+ },
+ Program(node) {
+ adjustTypeScriptEnumsInStatements(node.body);
+ adjustStaticMembersInStatements(node.body);
+ },
+ BlockStatement(node) {
+ adjustTypeScriptEnumsInStatements(node.body);
+ adjustStaticMembersInStatements(node.body);
+ },
+ CallExpression(node) {
+ if (isAlreadyEdited(node.start, node.end)) {
+ return;
+ }
+
+ // 1. Elide Angular Metadata check
+ let calleeName: string | undefined;
+ if (node.callee.type === 'Identifier') {
+ calleeName = node.callee.name;
+ } else if (
+ node.callee.type === 'MemberExpression' &&
+ node.callee.property.type === 'Identifier'
+ ) {
+ calleeName = node.callee.property.name;
+ }
+
+ if (calleeName && angularMetadataFunctions.has(calleeName)) {
+ const parentFunc = functionStack[functionStack.length - 1];
+ if (
+ parentFunc &&
+ (parentFunc.type === 'FunctionExpression' ||
+ parentFunc.type === 'ArrowFunctionExpression')
+ ) {
+ s.overwrite(node.start, node.end, 'void 0');
+ markEdited(node.start, node.end);
+
+ return;
+ }
+ }
+
+ // 2. Mark Top-Level Pure Functions check
+ if (!pureAnnotate || functionDepth > 0 || classDepth > 0 || topLevelSafeMode) {
+ return;
+ }
+
+ const callee = unwrapParentheses(node.callee);
+ if (
+ (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') &&
+ node.arguments.length !== 0
+ ) {
+ return;
+ }
+
+ if (
+ callee.type === 'Identifier' &&
+ (isTslibHelperName(callee.name) || isBabelHelperName(callee.name))
+ ) {
+ return;
+ }
+
+ if (!hasPureComment(node.start)) {
+ s.appendLeft(node.start, '/*#__PURE__*/ ');
+ }
+ },
+ NewExpression(node) {
+ if (
+ !pureAnnotate ||
+ functionDepth > 0 ||
+ classDepth > 0 ||
+ isAlreadyEdited(node.start, node.end)
+ ) {
+ return;
+ }
+
+ if (!topLevelSafeMode) {
+ if (!hasPureComment(node.start)) {
+ s.appendLeft(node.start, '/*#__PURE__*/ ');
+ }
+
+ return;
+ }
+
+ const callee = node.callee;
+ if (callee.type === 'Identifier' && sideEffectFreeConstructors.has(callee.name)) {
+ if (!hasPureComment(node.start)) {
+ s.appendLeft(node.start, '/*#__PURE__*/ ');
+ }
+ }
+ },
+ });
+
+ visitor.visit(program);
+
+ let map: string | undefined;
+ if (options.sourcemap) {
+ const rawMap = s.generateMap({ hires: true, source: filename });
+ const inputMap = loadInputSourceMap(filename, code);
+
+ if (inputMap) {
+ map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString();
+ } else {
+ map = rawMap.toString();
+ }
+ }
+
+ return {
+ code: s.toString(),
+ map,
+ };
+}
diff --git a/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_oxc_spec.ts
new file mode 100644
index 000000000000..0b91cc98aa45
--- /dev/null
+++ b/packages/angular/build/src/tools/babel/plugins/pure-toplevel-functions_oxc_spec.ts
@@ -0,0 +1,202 @@
+/**
+ * @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.dev/license
+ */
+
+import { transform } from './oxc-transform';
+
+function cleanCode(code: string): string {
+ return code
+ .replace(/\s+/g, '')
+ .replace(/"/g, "'")
+ .replace(/;}/g, '}')
+ .replace(/,([}\])])/g, '$1');
+}
+
+function testCase({
+ input,
+ expected,
+ options,
+}: {
+ input: string;
+ expected: string;
+ options?: { topLevelSafeMode: boolean };
+}): jasmine.ImplementationCallback {
+ return async () => {
+ const result = transform('test.js', input, {
+ sourcemap: false,
+ topLevelSafeMode: options?.topLevelSafeMode,
+ });
+ if (!result?.code) {
+ fail('Expected oxc-transform to return a transform result.');
+ } else {
+ const actualClean = cleanCode(result.code);
+ const expectedClean = cleanCode(expected);
+ expect(actualClean).toEqual(expectedClean);
+ }
+ };
+}
+
+function testCaseNoChange(input: string): jasmine.ImplementationCallback {
+ return testCase({ input, expected: input });
+}
+
+describe('pure-toplevel-functions oxc-transform implementation', () => {
+ it(
+ 'annotates top-level new expressions',
+ testCase({
+ input: 'var result = new SomeClass();',
+ expected: 'var result = /*#__PURE__*/ new SomeClass();',
+ }),
+ );
+
+ it(
+ 'annotates top-level function calls',
+ testCase({
+ input: 'var result = someCall();',
+ expected: 'var result = /*#__PURE__*/ someCall();',
+ }),
+ );
+
+ it(
+ 'annotates top-level IIFE assignments with no arguments',
+ testCase({
+ input: 'var SomeClass = (function () { function SomeClass() { } return SomeClass; })();',
+ expected:
+ 'var SomeClass = /*#__PURE__*/(function () { function SomeClass() { } return SomeClass; })();',
+ }),
+ );
+
+ it(
+ 'annotates top-level arrow-function-based IIFE assignments with no arguments',
+ testCase({
+ input: 'var SomeClass = (() => { function SomeClass() { } return SomeClass; })();',
+ expected:
+ 'var SomeClass = /*#__PURE__*/(() => { function SomeClass() { } return SomeClass; })();',
+ }),
+ );
+
+ it(
+ 'does not annotate top-level IIFE assignments with arguments',
+ testCaseNoChange(
+ 'var SomeClass = (function () { function SomeClass() { } return SomeClass; })(abc);',
+ ),
+ );
+
+ it(
+ 'does not annotate top-level arrow-function-based IIFE assignments with arguments',
+ testCaseNoChange(
+ 'var SomeClass = (() => { function SomeClass() { } return SomeClass; })(abc);',
+ ),
+ );
+
+ it(
+ 'does not annotate call expressions inside function declarations',
+ testCaseNoChange('function funcDecl() { const result = someFunction(); }'),
+ );
+
+ it(
+ 'does not annotate call expressions inside function expressions',
+ testCaseNoChange('const foo = function funcDecl() { const result = someFunction(); }'),
+ );
+
+ it(
+ 'does not annotate call expressions inside arrow functions',
+ testCaseNoChange('const foo = () => { const result = someFunction(); }'),
+ );
+
+ it(
+ 'does not annotate new expressions inside function declarations',
+ testCaseNoChange('function funcDecl() { const result = new SomeClass(); }'),
+ );
+
+ it(
+ 'does not annotate new expressions inside function expressions',
+ testCaseNoChange('const foo = function funcDecl() { const result = new SomeClass(); }'),
+ );
+
+ it(
+ 'does not annotate new expressions inside arrow functions',
+ testCaseNoChange('const foo = () => { const result = new SomeClass(); }'),
+ );
+
+ it(
+ 'does not annotate TypeScript helper functions (tslib)',
+ testCaseNoChange(`
+ class LanguageState {}
+ __decorate([
+ __metadata("design:type", Function),
+ __metadata("design:paramtypes", [Object]),
+ __metadata("design:returntype", void 0)
+ ], LanguageState.prototype, "checkLanguage", null);
+ `),
+ );
+
+ it(
+ 'does not annotate _defineProperty function',
+ testCaseNoChange(`
+ class LanguageState {}
+ _defineProperty(
+ LanguageState,
+ 'property',
+ 'value'
+ );
+ `),
+ );
+
+ it(
+ 'does not annotate object literal methods',
+ testCaseNoChange(`
+ const literal = {
+ method() {
+ var newClazz = new Clazz();
+ }
+ };
+ `),
+ );
+
+ it(
+ 'annotates helper functions with non-numeric suffixes',
+ testCase({
+ input: 'var result = __decorate$foo();',
+ expected: 'var result = /*#__PURE__*/ __decorate$foo();',
+ }),
+ );
+
+ it(
+ 'does not annotate helper functions with numeric suffixes',
+ testCaseNoChange('var result = __decorate$1();'),
+ );
+
+ describe('topLevelSafeMode: true', () => {
+ it(
+ 'annotates top-level `new InjectionToken` expressions',
+ testCase({
+ input: `const result = new InjectionToken('abc');`,
+ expected: `const result = /*#__PURE__*/ new InjectionToken('abc');`,
+ options: { topLevelSafeMode: true },
+ }),
+ );
+
+ it(
+ 'does not annotate other top-level `new` expressions',
+ testCase({
+ input: 'const result = new SomeClass();',
+ expected: 'const result = new SomeClass();',
+ options: { topLevelSafeMode: true },
+ }),
+ );
+
+ it(
+ 'does not annotate top-level function calls',
+ testCase({
+ input: 'const result = someCall();',
+ expected: 'const result = someCall();',
+ options: { topLevelSafeMode: true },
+ }),
+ );
+ });
+});
diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts
index f2e5c0e64886..734a8a994882 100644
--- a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts
+++ b/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts
@@ -42,7 +42,7 @@ export default async function transformJavaScript(
const { filename, data, ...options } = request;
const textData = typeof data === 'string' ? data : textDecoder.decode(data);
- const transformedData = await transformWithBabel(filename, textData, options);
+ const transformedData = await transformJavaScriptImpl(filename, textData, options);
// Transfer the data via `move` instead of cloning
return Piscina.move(textEncoder.encode(transformedData));
@@ -54,7 +54,7 @@ export default async function transformJavaScript(
let linkerPluginCreator:
typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | undefined;
-async function transformWithBabel(
+async function transformJavaScriptImpl(
filename: string,
data: string,
options: Omit,
@@ -64,7 +64,7 @@ async function transformWithBabel(
options.sourcemap &&
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
- const plugins: PluginItem[] = [];
+ const babelPlugins: PluginItem[] = [];
if (options.instrumentForCoverage) {
try {
@@ -85,7 +85,7 @@ async function transformWithBabel(
const { default: coveragePluginFactory } =
await import('../babel/plugins/add-code-coverage.js');
- plugins.push(coveragePluginFactory(programVisitor) as unknown as PluginItem);
+ babelPlugins.push(coveragePluginFactory(programVisitor) as unknown as PluginItem);
} catch (error) {
throw new Error(
`The 'istanbul-lib-instrument' package is required for code coverage but was not found. Please install the package.`,
@@ -97,47 +97,52 @@ async function transformWithBabel(
if (shouldLink) {
// Lazy load the linker plugin only when linking is required
const linkerPlugin = await createLinkerPlugin(options);
- plugins.push(linkerPlugin as unknown as PluginItem);
+ babelPlugins.push(linkerPlugin as unknown as PluginItem);
}
- if (options.advancedOptimizations) {
- const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } =
- await import('../babel/plugins');
+ let code = data;
+
+ // If Babel is needed, run it first
+ if (babelPlugins.length > 0) {
+ const result = await transformAsync(code, {
+ filename,
+ inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
+ sourceMaps: useInputSourcemap ? 'inline' : false,
+ compact: false,
+ configFile: false,
+ babelrc: false,
+ browserslistConfigFile: false,
+ plugins: babelPlugins,
+ });
+ code = result?.code ?? code;
+ }
+ // Run advanced optimizations using our fast oxc-transform
+ if (options.advancedOptimizations) {
+ const { transform } = await import('../babel/plugins/oxc-transform.js');
const sideEffectFree = options.sideEffects === false;
const safeAngularPackage =
sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
-
- plugins.push(
- [markTopLevelPure, { topLevelSafeMode: !safeAngularPackage }],
- elideAngularMetadata,
- adjustTypeScriptEnums,
- [adjustStaticMembers, { wrapDecorators: sideEffectFree }],
- );
- }
-
- // If no additional transformations are needed, return the data directly
- if (plugins.length === 0) {
- // Strip sourcemaps if they should not be used
- return useInputSourcemap ? data : removeSourceMappingURL(data);
+ const topLevelSafeMode = !safeAngularPackage;
+
+ const result = transform(filename, code, {
+ sourcemap: useInputSourcemap,
+ sideEffects: options.sideEffects,
+ jit: options.jit,
+ topLevelSafeMode,
+ });
+ code = result.code;
+
+ if (useInputSourcemap && result.map) {
+ // Strip old source map comment if Babel added one
+ code = removeSourceMappingURL(code);
+ const base64Map = Buffer.from(result.map).toString('base64');
+ code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`;
+ }
}
- const result = await transformAsync(data, {
- filename,
- inputSourceMap: (useInputSourcemap ? undefined : false) as undefined,
- sourceMaps: useInputSourcemap ? 'inline' : false,
- compact: false,
- configFile: false,
- babelrc: false,
- browserslistConfigFile: false,
- plugins,
- });
-
- const outputCode = result?.code ?? data;
-
- // Strip sourcemaps if they should not be used.
- // Babel will keep the original comments even if sourcemaps are disabled.
- return useInputSourcemap ? outputCode : removeSourceMappingURL(outputCode);
+ // Strip sourcemaps if they should not be used
+ return useInputSourcemap ? code : removeSourceMappingURL(code);
}
async function requiresLinking(path: string, source: string): Promise {
diff --git a/packages/angular/build/src/utils/source-map.ts b/packages/angular/build/src/utils/source-map.ts
index 354539a154ec..3b91bfb5b5ad 100644
--- a/packages/angular/build/src/utils/source-map.ts
+++ b/packages/angular/build/src/utils/source-map.ts
@@ -6,6 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
+import type { EncodedSourceMap } from '@ampproject/remapping';
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
/**
* Removes `//# sourceMappingURL=` comments safely from the given JavaScript code,
* ignoring any occurrences that are inside string literals, template literals, or block comments.
@@ -185,3 +190,90 @@ export function removeSourceMappingURL(code: string): string {
return result.join('');
}
+
+/**
+ * Finds, resolves, and loads the input sourcemap referenced in the code's trailing
+ * sourceMappingURL comment, if present. Supports inline base64 data URIs, local absolute
+ * file URLs, and relative/absolute filesystem paths.
+ */
+export function loadInputSourceMap(filename: string, code: string): EncodedSourceMap | undefined {
+ // Locate the last sourceMappingURL comment using lastIndexOf to avoid scanning
+ // the entire file with a regular expression (significant for large files).
+ const lastSourceMapIndex = code.lastIndexOf('//# sourceMappingURL=');
+ if (lastSourceMapIndex === -1) {
+ return undefined;
+ }
+
+ const urlLine = code.slice(lastSourceMapIndex + 21);
+
+ // Inline base64-encoded sourcemaps can be extremely large (up to megabytes).
+ // Parse them without regular expressions to avoid heavy backtracking and allocations.
+ if (urlLine.startsWith('data:application/json;')) {
+ const base64StartIndex = urlLine.indexOf('base64,');
+ if (base64StartIndex === -1) {
+ return undefined;
+ }
+
+ const payloadStart = base64StartIndex + 7;
+ let payloadEnd = urlLine.length;
+ // Find the first trailing whitespace character that marks the end of the base64 payload.
+ for (let i = payloadStart; i < urlLine.length; i++) {
+ const char = urlLine[i];
+ if (char === ' ' || char === '\r' || char === '\n' || char === '\t') {
+ payloadEnd = i;
+ break;
+ }
+ }
+
+ // Verify that everything after the base64 payload is trailing whitespace
+ // to ensure this is a valid trailing sourceMappingURL comment at the end of the file.
+ for (let i = payloadEnd; i < urlLine.length; i++) {
+ const char = urlLine[i];
+ if (char !== ' ' && char !== '\r' && char !== '\n' && char !== '\t') {
+ return undefined;
+ }
+ }
+
+ try {
+ // Extract the base64 payload and decode it directly into binary memory.
+ const base64Content = urlLine.slice(payloadStart, payloadEnd);
+
+ return JSON.parse(Buffer.from(base64Content, 'base64').toString('utf-8')) as EncodedSourceMap;
+ } catch {
+ return undefined;
+ }
+ }
+
+ // Non-inline sourcemap comments (always small, typically < 200 characters).
+ const urlMatch = /^([^\r\n\s]+)/.exec(urlLine);
+ if (!urlMatch) {
+ return undefined;
+ }
+
+ const url = urlMatch[1];
+ const remaining = urlLine.slice(url.length);
+ // Verify there is only whitespace after the URL to the end of the file.
+ if (!/^\s*$/.test(remaining)) {
+ return undefined;
+ }
+
+ if (url.startsWith('file://')) {
+ // Local absolute file URL scheme.
+ try {
+ const mapPath = fileURLToPath(url);
+ if (existsSync(mapPath)) {
+ return JSON.parse(readFileSync(mapPath, 'utf8')) as EncodedSourceMap;
+ }
+ } catch {}
+ } else if (!/^[a-z]+:\/\//i.test(url)) {
+ // Local relative or absolute filesystem path (percent-decoded as it originates from a URI).
+ try {
+ const mapPath = resolve(dirname(filename), decodeURIComponent(url));
+ if (existsSync(mapPath)) {
+ return JSON.parse(readFileSync(mapPath, 'utf8')) as EncodedSourceMap;
+ }
+ } catch {}
+ }
+
+ return undefined;
+}