From 0c7035ad0d79367aea6e9d70bc257dd7d79550f1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:44:27 -0400 Subject: [PATCH] refactor(@angular/build): lazily evaluate sideEffects in javascript transformer Previously, compiler-plugin.ts eagerly resolved sideEffects for every JavaScript file loaded during bundling by calling await hasSideEffects(request), which executes esbuild's asynchronous build.resolve to inspect package.json. Within the JavaScript transformer, sideEffects is only needed in two specific locations: when checking if a file belongs to node_modules/@angular/ to apply top-level pure function annotations, and when determining whether to wrap decorators in side-effect-free files that lack primary candidate tokens. The sideEffects check is now evaluated lazily. Method signatures on transformData and transformFile have been simplified to accept a TransformOptions interface with an optional async sideEffects resolver callback. Inside hasAdvancedOptimizationCandidates, the resolver is only evaluated if the file matches the node_modules/@angular/ path pattern or contains decorator tokens without primary optimization tokens. If the candidate check does not query sideEffects, the resolver is never invoked, eliminating unnecessary build.resolve calls for bypassed files, linker-only modules, and files matching primary optimization tokens. --- .../tools/esbuild/angular/compiler-plugin.ts | 24 +- .../tools/esbuild/javascript-transformer.ts | 171 ++++++------ .../esbuild/javascript-transformer_spec.ts | 255 ++++++++++++++---- 3 files changed, 313 insertions(+), 137 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index bcebf9b110a0..92af13dfe497 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -526,15 +526,11 @@ export function createCompilerPlugin( } else if (typeof contents === 'string' && (useTypeScriptTranspilation || isJS)) { // A string indicates untransformed output from the TS/NG compiler. // This step is unneeded when using esbuild transpilation. - const sideEffects = await hasSideEffects(request); - const instrumentForCoverage = pluginOptions.instrumentForCoverage?.(request); - contents = await javascriptTransformer.transformData( - request, - contents, - true /* skipLinker */, - sideEffects, - instrumentForCoverage, - ); + contents = await javascriptTransformer.transformData(request, contents, { + skipLinker: true, + sideEffects: () => hasSideEffects(request), + instrumentForCoverage: pluginOptions.instrumentForCoverage?.(request), + }); // Store as the returned Uint8Array to allow caching the fully transformed code typeScriptFileCache.set(request, contents); @@ -573,12 +569,10 @@ export function createCompilerPlugin( return profileAsync( 'NG_EMIT_JS*', async () => { - const sideEffects = await hasSideEffects(request); - const contents = await javascriptTransformer.transformFile( - request, - pluginOptions.jit, - sideEffects, - ); + const contents = await javascriptTransformer.transformFile(request, { + skipLinker: pluginOptions.jit, + sideEffects: () => hasSideEffects(request), + }); return { contents, diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts index 824ee5817606..7cd4ffd1bf3f 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts @@ -33,9 +33,7 @@ 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('|'), -); +const DECORATOR_REGEX = new RegExp(DECORATOR_TOKENS.join('|')); /** * Determines whether JavaScript code contains potential candidate constructs for advanced optimizations. @@ -43,43 +41,44 @@ const ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX = new RegExp( * * @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. + * @param sideEffects An optional lazy resolver callback that returns whether the file is considered side-effect free. * @returns True if the code may contain constructs that advanced optimizations can mutate. */ -function hasAdvancedOptimizationCandidates( +async function hasAdvancedOptimizationCandidates( filename: string, data: string | Uint8Array, - sideEffects?: boolean, -): boolean { + sideEffects?: () => Promise, +): Promise { // Side-effect-free @angular/ packages undergo top-level pure function annotations - if (sideEffects === false && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename)) { + if (/[\\/]node_modules[\\/]@angular[\\/]/.test(filename) && (await sideEffects?.()) === false) { return true; } if (typeof data === 'string') { - const regex = - sideEffects === false - ? ADVANCED_OPTIMIZATION_WITH_DECORATORS_REGEX - : ADVANCED_OPTIMIZATION_REGEX; + const hasDecorators = DECORATOR_REGEX.test(data); + if (hasDecorators && (await sideEffects?.()) === false) { + return true; + } - return regex.test(data); + return ADVANCED_OPTIMIZATION_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) { + for (const tokenBytes of DECORATOR_TOKEN_BYTES) { if (dataBuffer.includes(tokenBytes)) { - return true; + if ((await sideEffects?.()) === false) { + return true; + } + break; } } - if (sideEffects === false) { - for (const tokenBytes of DECORATOR_TOKEN_BYTES) { - if (dataBuffer.includes(tokenBytes)) { - return true; - } + for (const tokenBytes of ADVANCED_OPTIMIZATION_TOKEN_BYTES) { + if (dataBuffer.includes(tokenBytes)) { + return true; } } @@ -121,6 +120,23 @@ export interface JavaScriptTransformerOptions { jit?: boolean; } +/** + * Transformation options for an individual file or data transform request. + */ +export interface TransformOptions { + /** If true, bypass all Angular linker processing; if false, attempt linking. */ + skipLinker?: boolean; + + /** + * An optional lazy resolver callback that returns whether the file has side-effects. + * If it resolves to false, top-level pure function annotations and decorator wrapping may be applied. + */ + sideEffects?: () => Promise; + + /** If true, instrument the code for test coverage. */ + instrumentForCoverage?: boolean; +} + /** * A class that performs transformation of JavaScript files and raw data. * A worker pool is used to distribute the transformation actions and allow @@ -220,56 +236,14 @@ export class JavaScriptTransformer { * Performs JavaScript transformations on a file from the filesystem. * If no transformations are required, the data for the original file will be returned. * @param filename The full path to the file. - * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking. - * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped. + * @param options Transformation options specific to this file. * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result. */ - async transformFile( - filename: string, - skipLinker?: boolean, - sideEffects?: boolean, - instrumentForCoverage?: boolean, - ): Promise { + async transformFile(filename: string, options?: TransformOptions): Promise { return this.#runWithThrottle(async () => { const data = await readFile(filename); - let cacheKey: string | undefined; - if (this.cache) { - // Create a cache key from the file data and options that effect the output. - // NOTE: If additional options are added, this may need to be updated. - const hasher = createContentHash(); - hasher.update(`${!!skipLinker}--${!!sideEffects}`); - hasher.update(data); - hasher.update(this.#fileCacheKeyBase); - cacheKey = hasher.digest(); - - try { - const cached = await this.cache.get(cacheKey); - if (cached !== undefined) { - return cached; - } - } catch { - // Failure to get the value should not fail the transform - } - } - - const result = await this.transformData( - filename, - data, - !!skipLinker, - sideEffects, - instrumentForCoverage, - ); - - if (this.cache && cacheKey) { - try { - await this.cache.put(cacheKey, result); - } catch { - // Failure to store the value in the cache should not fail the transform - } - } - - return result; + return this.transformData(filename, data, options); }); } @@ -278,25 +252,36 @@ export class JavaScriptTransformer { * to exist on the filesystem. * @param filename The full path of the file represented by the data. * @param data The data of the file that should be transformed. - * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking. - * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped. + * @param options Transformation options specific to this file data. * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result. */ async transformData( filename: string, data: string | Uint8Array, - skipLinker: boolean, - sideEffects?: boolean, - instrumentForCoverage?: boolean, + options?: TransformOptions, ): Promise { - const shouldLink = !skipLinker && requiresLinking(filename, data); + let resolvedSideEffects: boolean | undefined; + let sideEffectsQueried = false; + + const sideEffectsGetter = options?.sideEffects + ? async () => { + if (!sideEffectsQueried) { + sideEffectsQueried = true; + resolvedSideEffects = await options.sideEffects?.(); + } + + return resolvedSideEffects; + } + : undefined; + + const shouldLink = !options?.skipLinker && requiresLinking(filename, data); const shouldOptimize = this.#commonOptions.advancedOptimizations && - hasAdvancedOptimizationCandidates(filename, data, sideEffects); + (await hasAdvancedOptimizationCandidates(filename, data, sideEffectsGetter)); // 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 && !shouldOptimize && !instrumentForCoverage) { + if (!shouldLink && !shouldOptimize && !options?.instrumentForCoverage) { const keepSourcemap = this.#commonOptions.sourcemap && (!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); @@ -308,6 +293,28 @@ export class JavaScriptTransformer { return keepSourcemap ? data : removeSourceMappingURL(data); } + let cacheKey: string | undefined; + if (this.cache) { + // Create a cache key from the file data and options that affect the output. + // NOTE: If additional options are added, this may need to be updated. + const hasher = createContentHash(); + hasher.update( + `${!options?.skipLinker}--${resolvedSideEffects === false}--${!!options?.instrumentForCoverage}`, + ); + hasher.update(data); + hasher.update(this.#fileCacheKeyBase); + cacheKey = hasher.digest(); + + try { + const cached = await this.cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + } catch { + // Failure to get the value should not fail the transform + } + } + // Only standalone (non-pooled) ArrayBuffers can be transferred across worker threads. // Node.js shares an internal 8KB ArrayBuffer pool for small buffers, and transferring // a pooled buffer will throw a DataCloneError because detaching it invalidates other slices. @@ -319,18 +326,28 @@ export class JavaScriptTransformer { data.byteLength === data.buffer.byteLength && !process.versions.pnp; - return this.#ensureWorkerPool().run( + const result = (await this.#ensureWorkerPool().run( { filename, data, skipLinker: !shouldLink, - sideEffects, - instrumentForCoverage, + sideEffects: resolvedSideEffects, + instrumentForCoverage: options?.instrumentForCoverage, }, { transferList: isTransferable ? [data.buffer] : undefined, }, - ); + )) as Uint8Array; + + if (this.cache && cacheKey) { + try { + await this.cache.put(cacheKey, result); + } catch { + // Failure to store the value in the cache should not fail the transform + } + } + + return result; } /** 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 3949c9efbbdc..b818077be49b 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts @@ -45,7 +45,7 @@ describe('JavaScriptTransformer sourcemaps', () => { const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); 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 result = await transformer.transformData('src/app.js', input, { skipLinker: true }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -86,11 +86,9 @@ describe('JavaScriptTransformer sourcemaps', () => { //# sourceMappingURL=data:application/json;base64,${base64Map} `; - const result = await transformer.transformData( - 'node_modules/my-lib/directive.js', - input, - false, - ); + const result = await transformer.transformData('node_modules/my-lib/directive.js', input, { + skipLinker: false, + }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -133,11 +131,9 @@ describe('JavaScriptTransformer sourcemaps', () => { //# sourceMappingURL=data:application/json;base64,${base64Map} `; - const result = await transformer.transformData( - 'node_modules/my-lib/component.js', - input, - false, - ); + const result = await transformer.transformData('node_modules/my-lib/component.js', input, { + skipLinker: false, + }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -158,7 +154,7 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const input = 'export class MyClass { static ɵprov = 42; }'; - const result = await transformer.transformData('src/app.js', input, true); + const result = await transformer.transformData('src/app.js', input, { skipLinker: true }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -187,13 +183,10 @@ describe('JavaScriptTransformer sourcemaps', () => { const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); const input = `export function add(a, b) { return a + b; }\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; - const result = await transformer.transformData( - 'src/counter.js', - input, - true, - undefined, - true /* instrumentForCoverage */, - ); + const result = await transformer.transformData('src/counter.js', input, { + skipLinker: true, + instrumentForCoverage: true, + }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -223,13 +216,10 @@ describe('JavaScriptTransformer sourcemaps', () => { const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); const input = `var x = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; - const result = await transformer.transformData( - 'src/app.js', - input, - true, - undefined, - true /* instrumentForCoverage */, - ); + const result = await transformer.transformData('src/app.js', input, { + skipLinker: true, + instrumentForCoverage: true, + }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -250,7 +240,7 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputBuffer = Buffer.from('export class MyClass { static ɵprov = 42; }', 'utf-8'); - const result = await transformer.transformData('src/app.js', inputBuffer, true); + const result = await transformer.transformData('src/app.js', inputBuffer, { skipLinker: true }); const text = Buffer.from(result).toString('utf-8'); const map = extractSourcemap(text); @@ -272,7 +262,9 @@ describe('JavaScriptTransformer sourcemaps', () => { 'console.log("hello");\n//# sourceMappingURL=app.js.map', 'utf-8', ); - const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, true); + const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, { + skipLinker: true, + }); const text = Buffer.from(result).toString('utf-8'); expect(text).toBe('console.log("hello");\n'); @@ -287,7 +279,9 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputBuffer = Buffer.from('console.log("hello");\nconst x = 1;', 'utf-8'); - const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, true); + const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, { + skipLinker: true, + }); expect(result).toBe(inputBuffer); }); @@ -301,11 +295,9 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputBuffer = Buffer.from('console.log("no linking required");\nconst x = 1;', 'utf-8'); - const result = await transformer.transformData( - 'node_modules/my-lib/lib.js', - inputBuffer, - false, // skipLinker: false - ); + const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, { + skipLinker: false, + }); expect(result).toBe(inputBuffer); }); @@ -322,7 +314,7 @@ describe('JavaScriptTransformer sourcemaps', () => { const result = await transformer.transformData( 'node_modules/@angular/core/fesm2022/core.mjs', inputBuffer, - false, + { skipLinker: false }, ); expect(result).toBe(inputBuffer); @@ -339,7 +331,9 @@ describe('JavaScriptTransformer sourcemaps', () => { const inputBuffer = Buffer.from('export const ɵɵngDeclareDirective = () => {};', 'utf-8'); for (const ext of ['.ts', '.tsx', '.mts', '.cts']) { - const result = await transformer.transformData(`src/app/directive${ext}`, inputBuffer, false); + const result = await transformer.transformData(`src/app/directive${ext}`, inputBuffer, { + skipLinker: false, + }); expect(result).toBe(inputBuffer); } @@ -368,7 +362,7 @@ describe('JavaScriptTransformer sourcemaps', () => { const result = await transformer.transformData( 'node_modules/@angular/compiler-cli/test.js', input, - false, + { skipLinker: false }, ); const text = Buffer.from(result).toString('utf-8'); @@ -389,7 +383,9 @@ describe('JavaScriptTransformer sourcemaps', () => { 'function add(a, b) { return a + b; }\nconst result = add(1, 2);', 'utf-8', ); - const result = await transformer.transformData('src/math.js', inputBuffer, true); + const result = await transformer.transformData('src/math.js', inputBuffer, { + skipLinker: true, + }); expect(result).toBe(inputBuffer); }); @@ -410,7 +406,9 @@ describe('JavaScriptTransformer sourcemaps', () => { }`, 'utf-8', ); - const result = await transformer.transformData('src/user.service.js', inputBuffer, true); + const result = await transformer.transformData('src/user.service.js', inputBuffer, { + skipLinker: true, + }); expect(result).toBe(inputBuffer); }); @@ -431,7 +429,9 @@ describe('JavaScriptTransformer sourcemaps', () => { }`, 'utf-8', ); - const result = await transformer.transformData('src/user.service.js', inputBuffer, true); + const result = await transformer.transformData('src/user.service.js', inputBuffer, { + skipLinker: true, + }); expect(result).toBe(inputBuffer); }); @@ -446,7 +446,9 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputBuffer = Buffer.from('export class MyComponent { static prop = 42; }', 'utf-8'); - const result = await transformer.transformData('src/component.js', inputBuffer, true); + const result = await transformer.transformData('src/component.js', inputBuffer, { + skipLinker: true, + }); expect(result).toBe(inputBuffer); }); @@ -461,7 +463,7 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const input = 'export class MyService { static ɵprov = true; }'; - const result = await transformer.transformData('src/service.js', input, true); + const result = await transformer.transformData('src/service.js', input, { skipLinker: true }); const text = Buffer.from(result).toString('utf-8'); expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); @@ -477,7 +479,10 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputBuffer = Buffer.from('const MyClass = __decorate([], class {});', 'utf-8'); - const result = await transformer.transformData('src/class.js', inputBuffer, true, false); + const result = await transformer.transformData('src/class.js', inputBuffer, { + skipLinker: true, + sideEffects: async () => false, + }); expect(result).not.toBe(inputBuffer); }); @@ -492,7 +497,9 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputString = 'function multiply(a, b) { return a * b; }'; - const result = await transformer.transformData('src/math.js', inputString, true); + const result = await transformer.transformData('src/math.js', inputString, { + skipLinker: true, + }); expect(Buffer.from(result).toString('utf-8')).toBe(inputString); }); @@ -507,10 +514,168 @@ describe('JavaScriptTransformer sourcemaps', () => { ); const inputString = 'export class MyService { static ɵprov = true; }'; - const result = await transformer.transformData('src/service.js', inputString, true); + const result = await transformer.transformData('src/service.js', inputString, { + skipLinker: true, + }); const text = Buffer.from(result).toString('utf-8'); expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); }); + + it('should not query sideEffects when no candidate tokens are present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queried = false; + const inputBuffer = Buffer.from('function add(a, b) { return a + b; }', 'utf-8'); + await transformer.transformData('src/math.js', inputBuffer, { + skipLinker: true, + sideEffects: async () => { + queried = true; + + return false; + }, + }); + + expect(queried).toBeFalse(); + }); + + it('should not query sideEffects when primary tokens are present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queried = false; + const input = 'export class MyService { static ɵprov = true; }'; + await transformer.transformData('src/service.js', input, { + skipLinker: true, + sideEffects: async () => { + queried = true; + + return false; + }, + }); + + expect(queried).toBeFalse(); + }); + + it('should query sideEffects for @angular/ packages and dispatch to worker when sideEffects is false', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queryCount = 0; + const inputBuffer = Buffer.from('export const foo = someCall();', 'utf-8'); + const result = await transformer.transformData( + '/node_modules/@angular/core/fesm2022/index.mjs', + inputBuffer, + { + skipLinker: true, + sideEffects: async () => { + queryCount++; + + return false; + }, + }, + ); + + expect(queryCount).toBe(1); + expect(result).not.toBe(inputBuffer); + }); + + it('should query sideEffects for decorator tokens and evaluate at most once', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queryCount = 0; + const inputBuffer = Buffer.from('const MyClass = __decorate([], class {});', 'utf-8'); + const result = await transformer.transformData('src/class.js', inputBuffer, { + skipLinker: true, + sideEffects: async () => { + queryCount++; + + return false; + }, + }); + + expect(queryCount).toBe(1); + expect(result).not.toBe(inputBuffer); + }); + + it('should query sideEffects and wrap decorators when both primary and decorator tokens are present', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queryCount = 0; + const input = ` + let MyService = class MyService { static ɵprov = true; }; + MyService = __decorate([], MyService); + `; + const result = await transformer.transformData('src/service.js', input, { + skipLinker: true, + sideEffects: async () => { + queryCount++; + + return false; + }, + }); + + expect(queryCount).toBe(1); + const text = Buffer.from(result).toString('utf-8'); + expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); + expect(text).toContain('__decorate'); + }); + + it('should query sideEffects when both primary and decorator tokens are present in Buffer', async () => { + transformer = new JavaScriptTransformer( + { + sourcemap: false, + advancedOptimizations: true, + }, + 1, + ); + + let queryCount = 0; + const inputBuffer = Buffer.from( + 'let MyService = class MyService { static ɵprov = true; };\nMyService = __decorate([], MyService);', + 'utf-8', + ); + const result = await transformer.transformData('src/service.js', inputBuffer, { + skipLinker: true, + sideEffects: async () => { + queryCount++; + + return false; + }, + }); + + expect(queryCount).toBe(1); + const text = Buffer.from(result).toString('utf-8'); + expect(text).toContain('let MyService = /*#__PURE__*/ (() => {'); + expect(text).toContain('__decorate'); + }); }); });