Skip to content
Open
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 @@ -89,6 +89,10 @@ export default async function transformJavaScript(
const transformedData = await transformJavaScriptImpl(filename, textData, options);

// Transfer the data via `move` instead of cloning
if (transformedData === textData && typeof data !== 'string') {
return Piscina.move(data);
}

return Piscina.move(textEncoder.encode(transformedData));
}

Expand Down
93 changes: 59 additions & 34 deletions packages/angular/build/src/tools/esbuild/javascript-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { removeSourceMappingURL } from '../../utils/source-map';
import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool';
import { Cache } from './cache';

const SOURCEMAP_COMMENT_BYTES = Buffer.from('sourceMappingURL=');

/**
* Transformation options that should apply to all transformed files and data.
*/
Expand Down Expand Up @@ -132,50 +134,39 @@ export class JavaScriptTransformer {
return this.#runWithThrottle(async () => {
const data = await readFile(filename);

let result;
let cacheKey;
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.
// TODO: Consider xxhash or similar instead of SHA256
const hash = createHash('sha256');
hash.update(`${!!skipLinker}--${!!sideEffects}`);
hash.update(data);
hash.update(this.#fileCacheKeyBase);
cacheKey = hash.digest('hex');

try {
result = await this.cache?.get(cacheKey);
const cached = await this.cache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
} catch {
// Failure to get the value should not fail the transform
}
}

if (result === undefined) {
// If there is no cache or no cached entry, process the file
result = (await this.#ensureWorkerPool().run(
{
filename,
data,
skipLinker,
sideEffects,
instrumentForCoverage,
...this.#commonOptions,
},
{
// The below is disable as with Yarn PNP this causes build failures with the below message
// `Unable to deserialize cloned data`.
transferList: process.versions.pnp ? undefined : [data.buffer],
},
)) as Uint8Array;

// If there is a cache then store the result
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
}
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
}
}

Expand All @@ -194,7 +185,7 @@ export class JavaScriptTransformer {
*/
async transformData(
filename: string,
data: string,
data: string | Uint8Array,
skipLinker: boolean,
sideEffects?: boolean,
instrumentForCoverage?: boolean,
Expand All @@ -206,18 +197,52 @@ export class JavaScriptTransformer {
this.#commonOptions.sourcemap &&
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));

return Buffer.from(keepSourcemap ? data : removeSourceMappingURL(data), 'utf-8');
if (typeof data === 'string') {
return Buffer.from(keepSourcemap ? data : removeSourceMappingURL(data), 'utf-8');
}

if (keepSourcemap) {
return data;
}

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

// Fast check on raw ASCII bytes to avoid UTF-8 string decoding if no comment exists.
if (dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES) === -1) {
return data;
}

const text = dataBuffer.toString('utf-8');
const stripped = removeSourceMappingURL(text);

return stripped === text ? data : Buffer.from(stripped, 'utf-8');
}

return this.#runWithThrottle(() =>
this.#ensureWorkerPool().run({
// 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.
// In addition, SharedArrayBuffers cannot be transferred, and Yarn PnP has deserialization issues.
const isTransferable =
typeof data !== 'string' &&
data.buffer instanceof ArrayBuffer &&
data.byteOffset === 0 &&
data.byteLength === data.buffer.byteLength &&
!process.versions.pnp;

return this.#ensureWorkerPool().run(
{
filename,
data,
skipLinker,
sideEffects,
instrumentForCoverage,
...this.#commonOptions,
}),
},
{
transferList: isTransferable ? [data.buffer] : undefined,
},
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,56 @@ describe('JavaScriptTransformer sourcemaps', () => {
expect(typeof map?.['mappings']).toBe('string');
expect((map?.['mappings'] as string).length).toBeGreaterThan(0);
});

it('should accept a Uint8Array input in transformData', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: true,
advancedOptimizations: true,
},
1,
);

const inputBuffer = Buffer.from('var x = new SomeClass();', 'utf-8');
const result = await transformer.transformData('src/app.js', inputBuffer, true);
const text = Buffer.from(result).toString('utf-8');
const map = extractSourcemap(text);

expect(map).toBeDefined();
expect(map?.['version']).toBe(3);
expect(map?.['sources']).toContain('src/app.js');
expect(typeof map?.['mappings']).toBe('string');
});

it('should strip trailing sourcemap comments from Uint8Array input on fast-path', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

const inputBuffer = Buffer.from(
'console.log("hello");\n//# sourceMappingURL=app.js.map',
'utf-8',
);
const result = await transformer.transformData('node_modules/my-lib/lib.js', inputBuffer, true);
const text = Buffer.from(result).toString('utf-8');

expect(text).toBe('console.log("hello");\n');
});

it('should return Uint8Array input untouched on fast-path when no sourcemap comment is present', async () => {
transformer = new JavaScriptTransformer(
{
sourcemap: false,
},
1,
);

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

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