Skip to content
Closed
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
50 changes: 34 additions & 16 deletions packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ interface InlineFileBatchRequest {
* The locale specifiers or locale objects that should be used during the inlining process of the file.
*/
locales: (string | { locale: string; translation?: Blob })[];

/**
* Whether the file data should be treated as ephemeral and not cached long-term in the Worker.
* Typically true when all remaining locales for the file are processed in a single batch.
*/
ephemeral?: boolean;
}

/**
Expand Down Expand Up @@ -124,23 +130,35 @@ const fileDataCache = new Map<string, Promise<CachedFileData>>();
const deserializedTranslations = new Map<string, Promise<Record<string, unknown>>>();

/**
* Retrieves the cached file data for a filename, loading and extracting it on the first request.
* Retrieves the file data for a filename, loading and extracting localization metadata.
* If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker.
* If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it
* to be garbage-collected once the batch request finishes.
*
* @param filename The name of the file to load.
* @returns The cached code and localization metadata.
* @param cache Whether to cache the loaded file data in the Worker's long-term cache.
* @returns The cached or newly extracted code and localization metadata.
*/
function getFileData(filename: string): Promise<CachedFileData> {
let fileDataPromise = fileDataCache.get(filename);
if (!fileDataPromise) {
fileDataPromise = (async () => {
const data = files.get(filename);
assert(data !== undefined, `Invalid inline request for file '${filename}'.`);

const code = await data.text();
const metadata = extractLocalizeMetadata(filename, code);

return { code, metadata };
})();
function loadFileData(filename: string, cache = true): Promise<CachedFileData> {
const existing = fileDataCache.get(filename);
if (existing) {
return existing;
}

const fileDataPromise = (async () => {
const data = files.get(filename);
assert(data !== undefined, `Invalid inline request for file '${filename}'.`);

const code = await data.text();
const metadata = extractLocalizeMetadata(filename, code);

return { code, metadata };
})();

if (cache) {
fileDataPromise.catch(() => {
fileDataCache.delete(filename);
});
fileDataCache.set(filename, fileDataPromise);
}

Expand Down Expand Up @@ -186,7 +204,7 @@ function loadTranslation(
* @returns An object containing the inlined file and optional map content.
*/
export default async function inlineFile(request: InlineFileRequest) {
const { code, metadata } = await getFileData(request.filename);
const { code, metadata } = await loadFileData(request.filename, true);

// Sourcemaps are parsed on demand per request rather than cached long-term to prevent
// monotonic memory growth as a worker processes multiple files across the build.
Expand Down Expand Up @@ -219,7 +237,7 @@ export default async function inlineFile(request: InlineFileRequest) {
export async function inlineFileBatch(
request: InlineFileBatchRequest,
): Promise<InlineFileBatchResult> {
const { code, metadata } = await getFileData(request.filename);
const { code, metadata } = await loadFileData(request.filename, !request.ephemeral);

// Parse the sourcemap once for the entire batch.
// It will naturally be garbage-collected after this batch action returns.
Expand Down
10 changes: 8 additions & 2 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 { extname, join } from 'node:path';
import { serialize } from 'node:v8';
import { calculateHash, createContentHash } from '../../utils/hash';
import { calculateHash, createContentHash, initializeHash } from '../../utils/hash';
import { WorkerPool } from '../../utils/worker-pool';
import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
Expand Down Expand Up @@ -377,6 +377,7 @@ export class I18nInliner {
const workerTasks: Promise<void>[] = [];

for (const [filename, entries] of uncachedByFile) {
const ephemeral = entries.length <= localesPerBatch;
for (let i = 0; i < entries.length; i += localesPerBatch) {
const batchEntries = entries.slice(i, i + localesPerBatch);
const task = (async () => {
Expand All @@ -387,6 +388,7 @@ export class I18nInliner {
locale: e.locale,
translation: e.translation,
})),
ephemeral,
},
{ name: 'inlineFileBatch' },
)) as {
Expand Down Expand Up @@ -518,7 +520,11 @@ export class I18nInliner {

// Initialize a persistent cache for i18n transformations.
try {
this.#cache = await createPersistentCacheStore(join(persistentCachePath, 'angular-i18n'));
const [, cache] = await Promise.all([
initializeHash(),
createPersistentCacheStore(join(persistentCachePath, 'angular-i18n')),
]);
this.#cache = cache;
} catch {
this.#cacheInitFailed = true;

Expand Down
5 changes: 5 additions & 0 deletions packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { transform } from 'esbuild';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { initializeHash } from '../../utils/hash';
import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
import { I18nInliner } from './i18n-inliner';

Expand Down Expand Up @@ -43,6 +44,10 @@ function findFile(outputFiles: BuildOutputFile[], path: string): BuildOutputFile
describe('I18nInliner', () => {
let inliner: I18nInliner | undefined;

beforeAll(async () => {
await initializeHash();
});

// A single thread is used throughout so that every file of every locale is inlined by the same
// Worker. Any translation state that a Worker retains between requests is then observable.
function createInliner(outputFiles: BuildOutputFile[]): I18nInliner {
Expand Down