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; +}