diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts index 3ba0dfff45b1..824ee5817606 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts @@ -16,6 +16,76 @@ import { Cache } from './cache'; const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare'; const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8'); +const ADVANCED_OPTIMIZATION_TOKENS = [ + 'ɵ', + 'InjectionToken', + 'INJECTOR_KEY', + 'ctorParameters', + 'decorators', + 'propDecorators', +] as const; + +const ADVANCED_OPTIMIZATION_TOKEN_BYTES = ADVANCED_OPTIMIZATION_TOKENS.map((token) => + Buffer.from(token, 'utf-8'), +); + +const DECORATOR_TOKENS = ['__decorate', '__esDecorate'] as const; +const DECORATOR_TOKEN_BYTES = DECORATOR_TOKENS.map((token) => Buffer.from(token, 'utf-8')); + +const ADVANCED_OPTIMIZATION_REGEX = new RegExp(ADVANCED_OPTIMIZATION_TOKENS.join('|')); +const ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX = new RegExp( + [...ADVANCED_OPTIMIZATION_TOKENS, ...DECORATOR_TOKENS].join('|'), +); + +/** + * Determines whether JavaScript code contains potential candidate constructs for advanced optimizations. + * When false, advanced optimizations can be bypassed without worker dispatch or AST parsing. + * + * @param filename The full path to the file. + * @param data The data (string or Buffer) of the file. + * @param sideEffects If false, indicates the file is considered side-effect free. + * @returns True if the code may contain constructs that advanced optimizations can mutate. + */ +function hasAdvancedOptimizationCandidates( + filename: string, + data: string | Uint8Array, + sideEffects?: boolean, +): boolean { + // Side-effect-free @angular/ packages undergo top-level pure function annotations + if (sideEffects === false && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename)) { + return true; + } + + if (typeof data === 'string') { + const regex = + sideEffects === false + ? ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX + : ADVANCED_OPTIMIZATION_REGEX; + + return regex.test(data); + } + + const dataBuffer = Buffer.isBuffer(data) + ? data + : Buffer.from(data.buffer, data.byteOffset, data.byteLength); + + for (const tokenBytes of ADVANCED_OPTIMIZATION_TOKEN_BYTES) { + if (dataBuffer.includes(tokenBytes)) { + return true; + } + } + + if (sideEffects === false) { + for (const tokenBytes of DECORATOR_TOKEN_BYTES) { + if (dataBuffer.includes(tokenBytes)) { + return true; + } + } + } + + return false; +} + /** * Determines whether JavaScript code requires Angular linker processing. * @@ -220,10 +290,13 @@ export class JavaScriptTransformer { instrumentForCoverage?: boolean, ): Promise { const shouldLink = !skipLinker && requiresLinking(filename, data); + const shouldOptimize = + this.#commonOptions.advancedOptimizations && + hasAdvancedOptimizationCandidates(filename, data, sideEffects); // Perform a quick test to determine if the data needs any transformations. // This allows directly returning the data without the worker communication overhead. - if (!shouldLink && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) { + if (!shouldLink && !shouldOptimize && !instrumentForCoverage) { const keepSourcemap = this.#commonOptions.sourcemap && (!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts index b1cdeec07b44..3949c9efbbdc 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts @@ -38,12 +38,12 @@ describe('JavaScriptTransformer sourcemaps', () => { const inputMap = { version: 3, sources: ['src/app.ts'], - sourcesContent: ['const x = new SomeClass();'], + sourcesContent: ['export class MyClass { static ɵprov = 42; }'], mappings: 'AAAA', names: [], }; const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); - const input = `var x = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; + const input = `export class MyClass { static ɵprov = 42; }\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; const result = await transformer.transformData('src/app.js', input, true); const text = Buffer.from(result).toString('utf-8'); @@ -157,7 +157,7 @@ describe('JavaScriptTransformer sourcemaps', () => { 1, ); - const input = 'var x = new SomeClass();'; + const input = 'export class MyClass { static ɵprov = 42; }'; const result = await transformer.transformData('src/app.js', input, true); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -249,7 +249,7 @@ describe('JavaScriptTransformer sourcemaps', () => { 1, ); - const inputBuffer = Buffer.from('var x = new SomeClass();', 'utf-8'); + const inputBuffer = Buffer.from('export class MyClass { static ɵprov = 42; }', 'utf-8'); const result = await transformer.transformData('src/app.js', inputBuffer, true); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -374,4 +374,143 @@ describe('JavaScriptTransformer sourcemaps', () => { expect(text).not.toContain('i0.ɵɵngDeclareDirective'); }); + + describe('advanced optimizations fast-path pre-filter', () => { + it('should bypass worker and return input buffer directly when no candidate tokens are present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from( + 'function add(a, b) { return a + b; }\nconst result = add(1, 2);', + 'utf-8', + ); + const result = await transformer.transformData('src/math.js', inputBuffer, true); + + expect(result).toBe(inputBuffer); + }); + + it('should bypass worker for standard classes without static properties', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from( + `export class UserService { + constructor(http) { this.http = http; } + getUser(id) { return this.http.get('/users/' + id); } + }`, + 'utf-8', + ); + const result = await transformer.transformData('src/user.service.js', inputBuffer, true); + + expect(result).toBe(inputBuffer); + }); + + it('should bypass worker for default exports without static properties or Angular metadata', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from( + `export default class UserService { + constructor(http) { this.http = http; } + getUser(id) { return this.http.get('/users/' + id); } + }`, + 'utf-8', + ); + const result = await transformer.transformData('src/user.service.js', inputBuffer, true); + + expect(result).toBe(inputBuffer); + }); + + it('should bypass worker for classes with static members when no Angular metadata is present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from('export class MyComponent { static prop = 42; }', 'utf-8'); + const result = await transformer.transformData('src/component.js', inputBuffer, true); + + expect(result).toBe(inputBuffer); + }); + + it('should dispatch to worker when Angular tokens are present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const input = 'export class MyService { static ɵprov = true; }'; + const result = await transformer.transformData('src/service.js', input, true); + const text = Buffer.from(result).toString('utf-8'); + + expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); + }); + + it('should dispatch to worker when decorator tokens are present and sideEffects is false', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputBuffer = Buffer.from('const MyClass = __decorate([], class {});', 'utf-8'); + const result = await transformer.transformData('src/class.js', inputBuffer, true, false); + + expect(result).not.toBe(inputBuffer); + }); + + it('should bypass worker and return converted buffer when no candidate tokens are present in string input', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputString = 'function multiply(a, b) { return a * b; }'; + const result = await transformer.transformData('src/math.js', inputString, true); + + expect(Buffer.from(result).toString('utf-8')).toBe(inputString); + }); + + it('should dispatch to worker when candidate tokens are present in string input', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + const inputString = 'export class MyService { static ɵprov = true; }'; + const result = await transformer.transformData('src/service.js', inputString, true); + const text = Buffer.from(result).toString('utf-8'); + + expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); + }); + }); });