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 @@ -39,14 +39,6 @@ const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const SOURCEMAP_COMMENT_BYTES = Buffer.from('//# sourceMappingURL=');

/**
* The function name prefix for all Angular partial compilation functions.
* Used to determine if linking of a JavaScript file is required.
* If any additional declarations are added or otherwise changed in the linker,
* the names MUST begin with this prefix.
*/
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';

async function instrumentCoverage(
filename: string,
data: string,
Expand Down Expand Up @@ -188,7 +180,7 @@ async function transformJavaScriptImpl(
data: string,
options: TransformOptions,
): Promise<string> {
const shouldLink = !options.skipLinker && requiresLinking(filename, data);
const shouldLink = !options.skipLinker;
const useInputSourcemap =
options.sourcemap &&
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
Expand Down Expand Up @@ -294,16 +286,3 @@ async function transformJavaScriptImpl(
// Strip sourcemaps if they should not be used
return options.isAlreadyStripped ? code : removeSourceMappingURL(code);
}

function requiresLinking(path: string, source: string): boolean {
// @angular/core and @angular/compiler will cause false positives
// Also, TypeScript files do not require linking
if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) {
return false;
}

// Check if the source code includes one of the declaration functions.
// There is a low chance of a false positive but the names are fairly unique
// and the result would be an unnecessary no-op additional plugin pass.
return source.includes(LINKER_DECLARATION_PREFIX);
}
33 changes: 31 additions & 2 deletions packages/angular/build/src/tools/esbuild/javascript-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool';
import { Cache } from './cache';

const SOURCEMAP_COMMENT_BYTES = Buffer.from('sourceMappingURL=');
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';
const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8');

/**
* Determines whether JavaScript code requires Angular linker processing.
*
* @param path The full path to the file.
* @param data The data (string or Buffer) of the file.
* @returns True if the code contains an Angular partial declaration; otherwise false.
*/
function requiresLinking(path: string, data: string | Uint8Array): boolean {
// @angular/core and @angular/compiler will cause false positives
// Also, TypeScript files do not require linking
if (/[\\/]@angular[\\/](?:compiler|core)[\\/]|\.[cm]?tsx?$/.test(path)) {
return false;
}

if (typeof data === 'string') {
return data.includes(LINKER_DECLARATION_PREFIX);
}

const dataBuffer = Buffer.isBuffer(data)
? data
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);

return dataBuffer.includes(LINKER_DECLARATION_PREFIX_BYTES);
}

/**
* Transformation options that should apply to all transformed files and data.
Expand Down Expand Up @@ -190,9 +217,11 @@ export class JavaScriptTransformer {
sideEffects?: boolean,
instrumentForCoverage?: boolean,
): Promise<Uint8Array> {
const shouldLink = !skipLinker && requiresLinking(filename, data);

// Perform a quick test to determine if the data needs any transformations.
// This allows directly returning the data without the worker communication overhead.
if (skipLinker && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) {
if (!shouldLink && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) {
const keepSourcemap =
this.#commonOptions.sourcemap &&
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
Expand Down Expand Up @@ -235,7 +264,7 @@ export class JavaScriptTransformer {
{
filename,
data,
skipLinker,
skipLinker: !shouldLink,
sideEffects,
instrumentForCoverage,
...this.#commonOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,87 @@ describe('JavaScriptTransformer sourcemaps', () => {

expect(result).toBe(inputBuffer);
});

it('should return Uint8Array untouched when skipLinker is false but file contains no linker declarations', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

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
);

expect(result).toBe(inputBuffer);
});

it('should bypass worker and skip linking for @angular/core and @angular/compiler paths', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

const inputBuffer = Buffer.from('export const ɵɵngDeclareDirective = () => {};', 'utf-8');
const result = await transformer.transformData(
'node_modules/@angular/core/fesm2022/core.mjs',
inputBuffer,
false,
);

expect(result).toBe(inputBuffer);
});

it('should bypass worker and skip linking for TypeScript file extensions (.ts, .tsx, .mts, .cts)', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

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);

expect(result).toBe(inputBuffer);
}
});

it('should not exclude packages with similar prefixes such as @angular/compiler-cli', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

const input = `
import * as i0 from "@angular/core";
export class MyDirective {}
MyDirective.ɵdir = i0.ɵɵngDeclareDirective({
minVersion: "12.0.0",
version: "14.0.0",
ngImport: i0,
type: MyDirective,
selector: "[my-dir]"
});
`;

const result = await transformer.transformData(
'node_modules/@angular/compiler-cli/test.js',
input,
false,
);
const text = Buffer.from(result).toString('utf-8');

expect(text).not.toContain('i0.ɵɵngDeclareDirective');
});
});
Loading