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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment thread
clydin marked this conversation as resolved.

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.
*
Expand Down Expand Up @@ -220,10 +290,13 @@ export class JavaScriptTransformer {
instrumentForCoverage?: boolean,
): Promise<Uint8Array> {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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__*/ (() => {');
});
});
Comment thread
clydin marked this conversation as resolved.
});
Loading