Skip to content

Commit

Permalink
refactor(@angular/build): improve BuildOutputFile property access
Browse files Browse the repository at this point in the history
The `BuildOutputFile` type's helper functions have been adjusted to cache
commonly accessed property values to avoid potentially expensive repeat
processing. This includes encoding/decoding UTF-8 content and calculating
hash values for the output file content. A size property has also been
added to allow consumers to more directly determine the byte size of the
output file. The size property is currently unused but will be leveraged
in forthcoming updates to bundle budgets and console info logging.

(cherry picked from commit e6d5c7e)
  • Loading branch information
clydin committed Jun 27, 2024
1 parent 7bdf314 commit 12b96d1
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 63 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/angular/build/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface BuildOutputFile extends OutputFile {
// (undocumented)
clone: () => BuildOutputFile;
// (undocumented)
readonly size: number;
// (undocumented)
type: BuildOutputFileType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../../tools/esbuild/bundler-context';
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
import { createOutputFileFromText } from '../../tools/esbuild/utils';
import { createOutputFile } from '../../tools/esbuild/utils';
import { maxWorkers } from '../../utils/environment-options';
import { prerenderPages } from '../../utils/server-rendering/prerender';
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
Expand Down Expand Up @@ -84,14 +84,14 @@ export async function executePostBundleSteps(

additionalHtmlOutputFiles.set(
indexHtmlOptions.output,
createOutputFileFromText(indexHtmlOptions.output, csrContent, BuildOutputFileType.Browser),
createOutputFile(indexHtmlOptions.output, csrContent, BuildOutputFileType.Browser),
);

if (ssrContent) {
const serverIndexHtmlFilename = 'index.server.html';
additionalHtmlOutputFiles.set(
serverIndexHtmlFilename,
createOutputFileFromText(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
createOutputFile(serverIndexHtmlFilename, ssrContent, BuildOutputFileType.Server),
);

ssrIndexContent = ssrContent;
Expand Down Expand Up @@ -131,7 +131,7 @@ export async function executePostBundleSteps(
for (const [path, content] of Object.entries(output)) {
additionalHtmlOutputFiles.set(
path,
createOutputFileFromText(path, content, BuildOutputFileType.Browser),
createOutputFile(path, content, BuildOutputFileType.Browser),
);
}
}
Expand All @@ -153,11 +153,7 @@ export async function executePostBundleSteps(
);

additionalOutputFiles.push(
createOutputFileFromText(
'ngsw.json',
serviceWorkerResult.manifest,
BuildOutputFileType.Browser,
),
createOutputFile('ngsw.json', serviceWorkerResult.manifest, BuildOutputFileType.Browser),
);
additionalAssets.push(...serviceWorkerResult.assetFiles);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export enum BuildOutputFileType {

export interface BuildOutputFile extends OutputFile {
type: BuildOutputFileType;
readonly size: number;
clone: () => BuildOutputFile;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { normalize } from 'node:path';
import type { ChangedFiles } from '../../tools/esbuild/watcher';
import type { SourceFileCache } from './angular/source-file-cache';
import type { BuildOutputFile, BuildOutputFileType, BundlerContext } from './bundler-context';
import { createOutputFileFromText } from './utils';
import { createOutputFile } from './utils';

export interface BuildOutputAsset {
source: string;
Expand Down Expand Up @@ -49,8 +49,8 @@ export class ExecutionResult {
private codeBundleCache?: SourceFileCache,
) {}

addOutputFile(path: string, content: string, type: BuildOutputFileType): void {
this.outputFiles.push(createOutputFileFromText(path, content, type));
addOutputFile(path: string, content: string | Uint8Array, type: BuildOutputFileType): void {
this.outputFiles.push(createOutputFile(path, content, type));
}

addAssets(assets: BuildOutputAsset[]): void {
Expand Down
6 changes: 3 additions & 3 deletions packages/angular/build/src/tools/esbuild/i18n-inliner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import assert from 'node:assert';
import Piscina from 'piscina';
import { BuildOutputFile, BuildOutputFileType } from './bundler-context';
import { createOutputFileFromText } from './utils';
import { createOutputFile } from './utils';

/**
* A keyword used to indicate if a JavaScript file may require inlining of translations.
Expand Down Expand Up @@ -139,9 +139,9 @@ export class I18nInliner {
const type = this.#fileToType.get(file);
assert(type !== undefined, 'localized file should always have a type' + file);

const resultFiles = [createOutputFileFromText(file, code, type)];
const resultFiles = [createOutputFile(file, code, type)];
if (map) {
resultFiles.push(createOutputFileFromText(file + '.map', map, type));
resultFiles.push(createOutputFile(file + '.map', map, type));
}

for (const message of messages) {
Expand Down
136 changes: 88 additions & 48 deletions packages/angular/build/src/tools/esbuild/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,64 +294,104 @@ export async function emitFilesToDisk<T = BuildOutputAsset | BuildOutputFile>(
}
}

export function createOutputFileFromText(
export function createOutputFile(
path: string,
text: string,
data: string | Uint8Array,
type: BuildOutputFileType,
): BuildOutputFile {
return {
path,
text,
type,
get hash() {
return createHash('sha256').update(this.text).digest('hex');
},
get contents() {
return Buffer.from(this.text, 'utf-8');
},
clone(): BuildOutputFile {
return createOutputFileFromText(this.path, this.text, this.type);
},
};
if (typeof data === 'string') {
let cachedContents: Uint8Array | null = null;
let cachedText: string | null = data;
let cachedHash: string | null = null;

return {
path,
type,
get contents(): Uint8Array {
cachedContents ??= new TextEncoder().encode(data);

return cachedContents;
},
set contents(value: Uint8Array) {
cachedContents = value;
cachedText = null;
},
get text(): string {
cachedText ??= new TextDecoder('utf-8').decode(this.contents);

return cachedText;
},
get size(): number {
return this.contents.byteLength;
},
get hash(): string {
cachedHash ??= createHash('sha256')
.update(cachedText ?? this.contents)
.digest('hex');

return cachedHash;
},
clone(): BuildOutputFile {
return createOutputFile(this.path, cachedText ?? this.contents, this.type);
},
};
} else {
let cachedContents = data;
let cachedText: string | null = null;
let cachedHash: string | null = null;

return {
get contents(): Uint8Array {
return cachedContents;
},
set contents(value: Uint8Array) {
cachedContents = value;
cachedText = null;
},
path,
type,
get size(): number {
return this.contents.byteLength;
},
get text(): string {
cachedText ??= new TextDecoder('utf-8').decode(this.contents);

return cachedText;
},
get hash(): string {
cachedHash ??= createHash('sha256').update(this.contents).digest('hex');

return cachedHash;
},
clone(): BuildOutputFile {
return createOutputFile(this.path, this.contents, this.type);
},
};
}
}

export function createOutputFileFromData(
path: string,
data: Uint8Array,
type: BuildOutputFileType,
): BuildOutputFile {
export function convertOutputFile(file: OutputFile, type: BuildOutputFileType): BuildOutputFile {
let { contents: cachedContents } = file;
let cachedText: string | null = null;

return {
path,
type,
get text() {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf-8');
get contents(): Uint8Array {
return cachedContents;
},
get hash() {
return createHash('sha256').update(this.text).digest('hex');
set contents(value: Uint8Array) {
cachedContents = value;
cachedText = null;
},
get contents() {
return data;
},
clone(): BuildOutputFile {
return createOutputFileFromData(this.path, this.contents, this.type);
hash: file.hash,
path: file.path,
type,
get size(): number {
return this.contents.byteLength;
},
};
}

export function convertOutputFile(file: OutputFile, type: BuildOutputFileType): BuildOutputFile {
const { path, contents, hash } = file;
get text(): string {
cachedText ??= new TextDecoder('utf-8').decode(this.contents);

return {
contents,
hash,
path,
type,
get text() {
return Buffer.from(
this.contents.buffer,
this.contents.byteOffset,
this.contents.byteLength,
).toString('utf-8');
return cachedText;
},
clone(): BuildOutputFile {
return convertOutputFile(this, this.type);
Expand Down

0 comments on commit 12b96d1

Please sign in to comment.