Skip to content
Draft
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
18 changes: 12 additions & 6 deletions packages/angular/build/src/builders/application/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { maxWorkers } from '../../utils/environment-options';
import { loadTranslations } from '../../utils/i18n-options';
import { createTranslationLoader } from '../../utils/load-translations';
import { createProjectResolver } from '../../utils/resolve-project';
import type { WorkerPool } from '../../utils/worker-pool';
import { executePostBundleSteps } from './execute-post-bundle';
import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options';

Expand All @@ -31,12 +32,14 @@ import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'
* @param options The normalized application builder options used to create the build.
* @param executionResult The result of an executed build.
* @param initialFiles A map containing initial file information for the executed build.
* @param workerPool An optional worker pool to use for running transformation tasks.
*/
export async function inlineI18n(
metafile: Metafile,
options: NormalizedApplicationBuildOptions,
executionResult: ExecutionResult,
initialFiles: Map<string, InitialFileRecord>,
workerPool?: WorkerPool,
): Promise<{
errors: string[];
warnings: string[];
Expand All @@ -45,12 +48,15 @@ export async function inlineI18n(
const { i18nOptions, baseHref, cacheOptions } = options;

// Create the multi-threaded inliner with common options.
const inliner = new I18nInliner({
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
maxConcurrency: maxWorkers,
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
localizeVersion: i18nOptions.localizeVersion,
});
const inliner = new I18nInliner(
{
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
maxConcurrency: maxWorkers,
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
localizeVersion: i18nOptions.localizeVersion,
},
workerPool,
);

const inlineResult: {
errors: string[];
Expand Down
22 changes: 16 additions & 6 deletions packages/angular/build/src/tools/esbuild/i18n-inliner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ interface UncachedLocaleEntry {
*/
export class I18nInliner {
#cacheInitFailed = false;
#workerPool: WorkerPool;
readonly #workerPool: WorkerPool;
readonly #ownsWorkerPool: boolean;
#cacheStore: PersistentCacheStore | undefined;
#transformedFileCache: Cache<TransformedFileResult> | undefined;
#translationCache: Cache<Uint8Array> | undefined;
Expand All @@ -197,20 +198,26 @@ export class I18nInliner {
return this.options.maxConcurrency ?? (this.#workerPool.maxThreads || 1);
}

constructor(private readonly options: I18nInlinerOptions) {
constructor(
private readonly options: I18nInlinerOptions,
workerPool?: WorkerPool,
) {
if (
options.maxConcurrency !== undefined &&
(!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1)
) {
throw new RangeError('options.maxConcurrency must be an integer greater than or equal to 1.');
}

this.#ownsWorkerPool = !workerPool;
// Piscina uses object spread against default options internally. Only define
// maxThreads when specified to avoid overwriting Piscina's default thread count
// with undefined.
this.#workerPool = new WorkerPool({
...(options.maxConcurrency !== undefined && { maxThreads: options.maxConcurrency }),
});
this.#workerPool =
workerPool ??
new WorkerPool({
...(options.maxConcurrency !== undefined && { maxThreads: options.maxConcurrency }),
});
}

#partitionFiles(files: Iterable<BuildOutputFile>): {
Expand Down Expand Up @@ -662,7 +669,10 @@ export class I18nInliner {
* @returns A void promise that resolves when closing is complete.
*/
async close(): Promise<void> {
await Promise.allSettled([this.#cacheStore?.close(), this.#workerPool.destroy()]);
await Promise.allSettled([
this.#cacheStore?.close(),
this.#ownsWorkerPool ? this.#workerPool.destroy() : undefined,
]);
}

/**
Expand Down
74 changes: 74 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 @@ -1092,4 +1092,78 @@ describe('I18nInliner', () => {
expect(maxThreadsSpy.calls.mostRecent().returnValue).toBeGreaterThanOrEqual(1);
});
});

describe('supplied workerPool', () => {
let externalPool: WorkerPool | undefined;

afterEach(async () => {
await externalPool?.destroy();
externalPool = undefined;
});

it('uses the supplied workerPool to execute transformation tasks', async () => {
externalPool = new WorkerPool({ maxThreads: 1 });
const runSpy = spyOn(externalPool, 'run').and.callThrough();

inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);

const files = [browserFile('main.js', GREETING_SOURCE)];
const results = await inliner.inlineAll(files, [
{
locale: 'fr',
translation: { greeting: translationFor('Bonjour') },
},
]);

expect(results.size).toBe(1);
expect(findFile(results.get('fr')?.outputFiles ?? [], 'main.js').text).toContain('"Bonjour"');
expect(runSpy).toHaveBeenCalled();
});

it('does not destroy the supplied workerPool on close()', async () => {
externalPool = new WorkerPool({ maxThreads: 1 });
const destroySpy = spyOn(externalPool, 'destroy').and.callThrough();

inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);
await inliner.close();

expect(destroySpy).not.toHaveBeenCalled();
});

it('destroys internally created workerPool on close()', async () => {
const destroySpy = spyOn(WorkerPool.prototype, 'destroy').and.callThrough();

inliner = new I18nInliner({ missingTranslation: 'warning', maxConcurrency: 1 });
await inliner.close();

expect(destroySpy).toHaveBeenCalled();
});

it('supports multiple sequential inlining passes with the same supplied workerPool', async () => {
externalPool = new WorkerPool({ maxThreads: 1 });

inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);

const files1 = [browserFile('main.js', GREETING_SOURCE)];
const results1 = await inliner.inlineAll(files1, [
{
locale: 'fr',
translation: { greeting: translationFor('Bonjour') },
},
]);
expect(findFile(results1.get('fr')?.outputFiles ?? [], 'main.js').text).toContain(
'"Bonjour"',
);

// Second inlining pass on the same pool
const files2 = [browserFile('main.js', GREETING_SOURCE)];
const results2 = await inliner.inlineAll(files2, [
{
locale: 'es',
translation: { greeting: translationFor('Hola') },
},
]);
expect(findFile(results2.get('es')?.outputFiles ?? [], 'main.js').text).toContain('"Hola"');
});
});
});
Loading