Skip to content

Commit 1d5377b

Browse files
committed
refactor(@angular/build): pass files directly to inlineAll in i18n inliner
Pass build files directly into inlineAll and inlineForLocale via the files parameter instead of requiring them in the I18nInliner constructor options. Because file data is passed on-demand to workers per batch request via Blobs, the WorkerPool has no dependency on build files during initialization. Passing files directly to inlineAll decouples the inliner and worker pool lifecycle from individual build runs, allowing inliner and worker pool reuse across watch mode rebuilds. It also eliminates retention of previous build file buffers on the I18nInliner instance and removes the need to pass empty outputFiles arrays when only inlineTemplateUpdate is called.
1 parent f329174 commit 1d5377b

3 files changed

Lines changed: 226 additions & 179 deletions

File tree

packages/angular/build/src/builders/application/i18n.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,10 @@ export async function inlineI18n(
4444
}> {
4545
const { i18nOptions, baseHref, cacheOptions } = options;
4646

47-
// Create the multi-threaded inliner with common options and the files generated from the build.
47+
// Create the multi-threaded inliner with common options.
4848
const inliner = new I18nInliner(
4949
{
5050
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
51-
outputFiles: executionResult.outputFiles,
5251
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
5352
localizeVersion: i18nOptions.localizeVersion,
5453
},
@@ -92,7 +91,7 @@ export async function inlineI18n(
9291
};
9392
});
9493

95-
const inlinedLocales = await inliner.inlineAll(localesToInline);
94+
const inlinedLocales = await inliner.inlineAll(executionResult.outputFiles, localesToInline);
9695

9796
for (const locale of i18nOptions.inlineLocales) {
9897
const localeInlineResult = inlinedLocales.get(locale);

packages/angular/build/src/tools/esbuild/i18n-inliner.ts

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ async function serializeTranslation(
9999
*/
100100
export interface I18nInlinerOptions {
101101
missingTranslation: 'error' | 'warning' | 'ignore';
102-
outputFiles: BuildOutputFile[];
103102
persistentCachePath?: string;
104103
localizeVersion?: string;
105104
}
@@ -165,19 +164,36 @@ export class I18nInliner {
165164
#transformedFileCache: Cache<TransformedFileResult> | undefined;
166165
#translationCache: Cache<Uint8Array> | undefined;
167166
#generation = 0;
168-
readonly #localizeFiles: ReadonlyMap<string, BuildOutputFile>;
169-
readonly #unmodifiedFiles: Array<BuildOutputFile>;
170167

171168
constructor(
172169
private readonly options: I18nInlinerOptions,
173170
maxThreads?: number,
174171
) {
175-
this.#unmodifiedFiles = [];
176-
const { outputFiles, missingTranslation } = options;
177-
const files = new Map<string, BuildOutputFile>();
172+
const { missingTranslation } = options;
173+
174+
this.#workerPool = new WorkerPool({
175+
filename: require.resolve('./i18n-inliner-worker'),
176+
maxThreads,
177+
// Extract options to ensure only the named options are serialized and sent to the worker
178+
workerData: {
179+
missingTranslation,
180+
},
181+
});
182+
}
178183

179-
const pendingMaps = [];
180-
for (const file of outputFiles) {
184+
#partitionFiles(files: Iterable<BuildOutputFile>): {
185+
filenames: string[];
186+
localizeFiles: Map<string, BuildOutputFile>;
187+
localizeMaps: Map<string, BuildOutputFile>;
188+
unmodifiedFiles: BuildOutputFile[];
189+
} {
190+
const filenames: string[] = [];
191+
const localizeFiles = new Map<string, BuildOutputFile>();
192+
const localizeMaps = new Map<string, BuildOutputFile>();
193+
const unmodifiedFiles: BuildOutputFile[] = [];
194+
195+
const pendingMaps: BuildOutputFile[] = [];
196+
for (const file of files) {
181197
if (file.type === BuildOutputFileType.Root || file.type === BuildOutputFileType.ServerRoot) {
182198
// Skip also the server entry-point.
183199
// Skip stats and similar files.
@@ -193,7 +209,8 @@ export class I18nInliner {
193209
const hasLocalize = contentBuffer.includes(LOCALIZE_KEYWORD);
194210

195211
if (hasLocalize) {
196-
files.set(file.path, file);
212+
localizeFiles.set(file.path, file);
213+
filenames.push(file.path);
197214

198215
continue;
199216
}
@@ -204,28 +221,20 @@ export class I18nInliner {
204221
continue;
205222
}
206223

207-
this.#unmodifiedFiles.push(file);
224+
unmodifiedFiles.push(file);
208225
}
209226

210227
// Check if any pending map files should be processed by checking if the parent JS file is present
211228
for (const file of pendingMaps) {
212-
if (files.has(file.path.slice(0, -4))) {
213-
files.set(file.path, file);
229+
const jsPath = file.path.slice(0, -4);
230+
if (localizeFiles.has(jsPath)) {
231+
localizeMaps.set(jsPath, file);
214232
} else {
215-
this.#unmodifiedFiles.push(file);
233+
unmodifiedFiles.push(file);
216234
}
217235
}
218236

219-
this.#localizeFiles = files;
220-
221-
this.#workerPool = new WorkerPool({
222-
filename: require.resolve('./i18n-inliner-worker'),
223-
maxThreads,
224-
// Extract options to ensure only the named options are serialized and sent to the worker
225-
workerData: {
226-
missingTranslation,
227-
},
228-
});
237+
return { filenames, localizeFiles, localizeMaps, unmodifiedFiles };
229238
}
230239

231240
/**
@@ -234,10 +243,12 @@ export class I18nInliner {
234243
* An adaptive 2D task-partitioning algorithm distributes (files x locales) work units
235244
* across all worker threads while caching AST metadata and sourcemaps in worker memory.
236245
*
246+
* @param files The build output files to transform.
237247
* @param locales The locales and translations to inline.
238248
* @returns A map of locale names to their inlined output files and diagnostics.
239249
*/
240250
async inlineAll(
251+
files: Iterable<BuildOutputFile>,
241252
locales: Iterable<LocaleInlineOptions>,
242253
): Promise<Map<string, LocaleInlineResult>> {
243254
await this.initCache();
@@ -250,16 +261,14 @@ export class I18nInliner {
250261
return new Map();
251262
}
252263

264+
const { filenames, localizeFiles, localizeMaps, unmodifiedFiles } = this.#partitionFiles(files);
265+
253266
const fileResultsByLocale = new Map<string, Map<string, TransformedFileResult>>();
254267
for (const { locale } of localeList) {
255268
assert(!fileResultsByLocale.has(locale), 'Duplicate locale provided to inliner: ' + locale);
256269
fileResultsByLocale.set(locale, new Map());
257270
}
258271

259-
const filenames = Array.from(this.#localizeFiles.keys()).filter(
260-
(name) => !name.endsWith('.map'),
261-
);
262-
263272
// Process locales in sliding windows to cap peak worker memory.
264273
// Ensure the window has at least enough locales to saturate all available workers on high-core machines.
265274
const windowSize = Math.max(DEFAULT_LOCALE_WINDOW_SIZE, this.#workerPool.maxThreads || 1);
@@ -304,7 +313,7 @@ export class I18nInliner {
304313
const cacheChecks: Promise<void>[] = [];
305314

306315
for (const filename of filenames) {
307-
const file = this.#localizeFiles.get(filename);
316+
const file = localizeFiles.get(filename);
308317
assert(file !== undefined, 'Localize file must exist: ' + filename);
309318

310319
const fileEntriesPromises = windowLocales.map(
@@ -361,6 +370,8 @@ export class I18nInliner {
361370
// Adaptive 2D Sharding for uncached tasks in this window
362371
if (uncachedByFile.size > 0) {
363372
await this.#processUncachedBatches(
373+
localizeFiles,
374+
localizeMaps,
364375
uncachedByFile,
365376
fileResultsByLocale,
366377
activeLocales,
@@ -381,7 +392,7 @@ export class I18nInliner {
381392

382393
if (fileResults) {
383394
for (const filename of filenames) {
384-
const originalFile = this.#localizeFiles.get(filename);
395+
const originalFile = localizeFiles.get(filename);
385396
assert(originalFile !== undefined, 'Localize file must exist: ' + filename);
386397

387398
const fileResult = fileResults.get(filename);
@@ -396,7 +407,7 @@ export class I18nInliner {
396407
outputFiles.push(originalFile.clone());
397408
}
398409

399-
const originalMap = this.#localizeFiles.get(filename + '.map');
410+
const originalMap = localizeMaps.get(filename);
400411
if (fileResult.map !== undefined) {
401412
outputFiles.push(createOutputFile(filename + '.map', fileResult.map, type));
402413
} else if (originalMap !== undefined) {
@@ -414,7 +425,7 @@ export class I18nInliner {
414425
}
415426

416427
// Include cloned unmodified files for every locale
417-
outputFiles.push(...this.#unmodifiedFiles.map((file) => file.clone()));
428+
outputFiles.push(...unmodifiedFiles.map((file) => file.clone()));
418429

419430
resultsByLocale.set(locale, {
420431
outputFiles,
@@ -427,6 +438,8 @@ export class I18nInliner {
427438
}
428439

429440
async #processUncachedBatches(
441+
localizeFiles: Map<string, BuildOutputFile>,
442+
localizeMaps: Map<string, BuildOutputFile>,
430443
uncachedByFile: Map<string, UncachedLocaleEntry[]>,
431444
fileResultsByLocale: Map<string, Map<string, TransformedFileResult>>,
432445
activeLocales?: string[],
@@ -438,7 +451,7 @@ export class I18nInliner {
438451
// Extract file data and identify the heaviest file size in a single pass
439452
let maxFileSize = 0;
440453
const sortedFiles = Array.from(uncachedByFile, ([filename, entries]) => {
441-
const codeFile = this.#localizeFiles.get(filename);
454+
const codeFile = localizeFiles.get(filename);
442455
assert(codeFile !== undefined, 'Localize file must exist: ' + filename);
443456
const fileSize = codeFile.contents.byteLength;
444457
if (fileSize > maxFileSize) {
@@ -456,7 +469,7 @@ export class I18nInliner {
456469
const workerTasks: Promise<void>[] = [];
457470

458471
for (const { filename, entries, codeFile, fileSize } of sortedFiles) {
459-
const mapFile = this.#localizeFiles.get(filename + '.map');
472+
const mapFile = localizeMaps.get(filename);
460473
const codeBlob = new Blob([codeFile.contents]);
461474
const mapBlob = mapFile ? new Blob([mapFile.contents]) : undefined;
462475

@@ -548,20 +561,21 @@ export class I18nInliner {
548561
}
549562

550563
/**
551-
* Performs inlining of translations for the provided locale and translations. The files that
552-
* are processed originate from the files passed to the class constructor and filter by presence
553-
* of the localize function keyword.
564+
* Performs inlining of translations for the provided locale and translations.
565+
*
566+
* @param files The build output files to transform.
554567
* @param locale The string representing the locale to inline.
555568
* @param translation The translation messages to use when inlining.
556569
* @param translationIntegrity An optional integrity value for the translation messages to use for caching.
557570
* @returns A promise that resolves to an array of OutputFiles representing a translated result.
558571
*/
559572
async inlineForLocale(
573+
files: Iterable<BuildOutputFile>,
560574
locale: string,
561575
translation: Record<string, ɵParsedTranslation> | undefined,
562576
translationIntegrity?: string,
563577
): Promise<LocaleInlineResult> {
564-
const results = await this.inlineAll([{ locale, translation, translationIntegrity }]);
578+
const results = await this.inlineAll(files, [{ locale, translation, translationIntegrity }]);
565579
const result = results.get(locale);
566580
assert(result !== undefined, `Result for locale '${locale}' should be present.`);
567581

0 commit comments

Comments
 (0)