Skip to content

Commit bc831e8

Browse files
clydinvikerman
authored andcommitted
feat(@angular-devkit/build-angular): support parallel i18n localization
1 parent 987aebe commit bc831e8

File tree

6 files changed

+266
-73
lines changed

6 files changed

+266
-73
lines changed

packages/angular_devkit/build_angular/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@angular-devkit/build-webpack": "0.0.0",
1313
"@angular-devkit/core": "0.0.0",
1414
"@babel/core": "7.6.4",
15+
"@babel/generator": "7.6.4",
1516
"@babel/preset-env": "7.6.3",
1617
"@ngtools/webpack": "0.0.0",
1718
"ajv": "6.10.2",
@@ -33,6 +34,7 @@
3334
"less-loader": "5.0.0",
3435
"license-webpack-plugin": "2.1.3",
3536
"loader-utils": "1.2.3",
37+
"magic-string": "0.25.4",
3638
"mini-css-extract-plugin": "0.8.0",
3739
"minimatch": "3.0.4",
3840
"parse5": "4.0.0",

packages/angular_devkit/build_angular/src/browser/action-executor.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,25 @@
88
import JestWorker from 'jest-worker';
99
import * as os from 'os';
1010
import * as path from 'path';
11-
import { ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle';
11+
import * as v8 from 'v8';
12+
import { InlineOptions, ProcessBundleOptions, ProcessBundleResult } from '../utils/process-bundle';
1213
import { BundleActionCache } from './action-cache';
1314

15+
const hasThreadSupport = (() => {
16+
try {
17+
require('worker_threads');
18+
19+
return true;
20+
} catch {
21+
return false;
22+
}
23+
})();
24+
25+
// This is used to normalize serialization messaging across threads and processes
26+
// Threads use the structured clone algorithm which handles more types
27+
// Processes use JSON which is much more limited
28+
const serialize = ((v8 as unknown) as { serialize(value: unknown): Buffer }).serialize;
29+
1430
let workerFile = require.resolve('../utils/process-bundle');
1531
workerFile =
1632
path.extname(workerFile) === '.ts'
@@ -41,8 +57,8 @@ export class BundleActionExecutor {
4157

4258
// larger files are processed in a separate process to limit memory usage in the main process
4359
return (this.largeWorker = new JestWorker(workerFile, {
44-
exposedMethods: ['process'],
45-
setupArgs: [this.workerOptions],
60+
exposedMethods: ['process', 'inlineLocales'],
61+
setupArgs: [[...serialize(this.workerOptions)]],
4662
}));
4763
}
4864

@@ -54,11 +70,10 @@ export class BundleActionExecutor {
5470
// small files are processed in a limited number of threads to improve speed
5571
// The limited number also prevents a large increase in memory usage for an otherwise short operation
5672
return (this.smallWorker = new JestWorker(workerFile, {
57-
exposedMethods: ['process'],
58-
setupArgs: [this.workerOptions],
73+
exposedMethods: ['process', 'inlineLocales'],
74+
setupArgs: hasThreadSupport ? [this.workerOptions] : [[...serialize(this.workerOptions)]],
5975
numWorkers: os.cpus().length < 2 ? 1 : 2,
60-
// Will automatically fallback to processes if not supported
61-
enableWorkerThreads: true,
76+
enableWorkerThreads: hasThreadSupport,
6277
}));
6378
}
6479

@@ -71,7 +86,7 @@ export class BundleActionExecutor {
7186
}
7287
}
7388

74-
async process(action: ProcessBundleOptions) {
89+
async process(action: ProcessBundleOptions): Promise<ProcessBundleResult> {
7590
const cacheKeys = this.cache.generateCacheKeys(action);
7691
action.cacheKeys = cacheKeys;
7792

@@ -86,10 +101,27 @@ export class BundleActionExecutor {
86101
return this.executeAction<ProcessBundleResult>('process', action);
87102
}
88103

89-
async *processAll(actions: Iterable<ProcessBundleOptions>) {
90-
const executions = new Map<Promise<ProcessBundleResult>, Promise<ProcessBundleResult>>();
104+
processAll(actions: Iterable<ProcessBundleOptions>): AsyncIterable<ProcessBundleResult> {
105+
return BundleActionExecutor.executeAll(actions, action => this.process(action));
106+
}
107+
108+
async inline(
109+
action: InlineOptions,
110+
): Promise<{ file: string; diagnostics: { type: string; message: string }[]; count: number; }> {
111+
return this.executeAction('inlineLocales', action);
112+
}
113+
114+
inlineAll(actions: Iterable<InlineOptions>) {
115+
return BundleActionExecutor.executeAll(actions, action => this.inline(action));
116+
}
117+
118+
private static async *executeAll<I, O>(
119+
actions: Iterable<I>,
120+
executor: (action: I) => Promise<O>,
121+
): AsyncIterable<O> {
122+
const executions = new Map<Promise<O>, Promise<O>>();
91123
for (const action of actions) {
92-
const execution = this.process(action);
124+
const execution = executor(action);
93125
executions.set(
94126
execution,
95127
execution.then(result => {
@@ -105,7 +137,7 @@ export class BundleActionExecutor {
105137
}
106138
}
107139

108-
stop() {
140+
stop(): void {
109141
if (this.largeWorker) {
110142
this.largeWorker.end();
111143
}

packages/angular_devkit/build_angular/src/browser/index.ts

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
9-
import {
10-
EmittedFiles,
11-
WebpackLoggingCallback,
12-
runWebpack,
13-
} from '@angular-devkit/build-webpack';
9+
import { EmittedFiles, WebpackLoggingCallback, runWebpack } from '@angular-devkit/build-webpack';
1410
import { join, json, logging, normalize, tags, virtualFs } from '@angular-devkit/core';
1511
import { NodeJsSyncHost } from '@angular-devkit/core/node';
1612
import * as findCacheDirectory from 'find-cache-dir';
@@ -58,6 +54,7 @@ import { copyAssets } from '../utils/copy-assets';
5854
import { I18nOptions, createI18nOptions } from '../utils/i18n-options';
5955
import { createTranslationLoader } from '../utils/load-translations';
6056
import {
57+
InlineOptions,
6158
ProcessBundleFile,
6259
ProcessBundleOptions,
6360
ProcessBundleResult,
@@ -447,76 +444,103 @@ export function buildWebpackBrowser(
447444
}
448445

449446
context.logger.info('ES5 bundle generation complete.');
450-
} finally {
451-
executor.stop();
452-
}
453-
454-
if (i18n.shouldInline) {
455-
context.logger.info('Generating localized bundles...');
456-
457-
const localize = await import('@angular/localize/src/tools/src/translate/main');
458-
const localizeDiag = await import('@angular/localize/src/tools/src/diagnostics');
459447

460-
const diagnostics = new localizeDiag.Diagnostics();
461-
const translationFilePaths = [];
462-
let handleSourceLocale = false;
463-
for (const locale of i18n.inlineLocales) {
464-
if (locale === i18n.sourceLocale) {
465-
handleSourceLocale = true;
466-
continue;
467-
}
468-
translationFilePaths.push(i18n.locales[locale].file);
469-
}
448+
if (i18n.shouldInline) {
449+
context.logger.info('Generating localized bundles...');
470450

471-
if (translationFilePaths.length > 0) {
472-
const sourceFilePaths = [];
451+
const inlineActions: InlineOptions[] = [];
452+
const processedFiles = new Set<string>();
473453
for (const result of processResults) {
474454
if (result.original) {
475-
sourceFilePaths.push(result.original.filename);
455+
inlineActions.push({
456+
filename: path.basename(result.original.filename),
457+
code: fs.readFileSync(result.original.filename, 'utf8'),
458+
outputPath: baseOutputPath,
459+
es5: false,
460+
missingTranslation: options.i18nMissingTranslation,
461+
});
462+
processedFiles.add(result.original.filename);
476463
}
477464
if (result.downlevel) {
478-
sourceFilePaths.push(result.downlevel.filename);
465+
inlineActions.push({
466+
filename: path.basename(result.downlevel.filename),
467+
code: fs.readFileSync(result.downlevel.filename, 'utf8'),
468+
outputPath: baseOutputPath,
469+
es5: true,
470+
missingTranslation: options.i18nMissingTranslation,
471+
});
472+
processedFiles.add(result.downlevel.filename);
479473
}
480474
}
475+
476+
let hasErrors = false;
481477
try {
482-
localize.translateFiles({
483-
// tslint:disable-next-line: no-non-null-assertion
484-
sourceRootPath: webpackStats.outputPath!,
485-
sourceFilePaths,
486-
translationFilePaths,
487-
outputPathFn: (locale, relativePath) =>
488-
path.join(baseOutputPath, locale, relativePath),
489-
diagnostics,
490-
missingTranslation: options.i18nMissingTranslation || 'warning',
491-
sourceLocale: handleSourceLocale ? i18n.sourceLocale : undefined,
492-
});
478+
for (const locale of i18n.inlineLocales) {
479+
const localeOutputPath = path.join(baseOutputPath, locale);
480+
if (!fs.existsSync(localeOutputPath)) {
481+
fs.mkdirSync(localeOutputPath, { recursive: true });
482+
}
483+
}
484+
485+
for await (const result of executor.inlineAll(inlineActions)) {
486+
if (options.verbose) {
487+
context.logger.info(
488+
`Localized "${result.file}" [${result.count} translation(s)].`,
489+
);
490+
}
491+
for (const diagnostic of result.diagnostics) {
492+
if (diagnostic.type === 'error') {
493+
hasErrors = true;
494+
context.logger.error(diagnostic.message);
495+
} else {
496+
context.logger.warn(diagnostic.message);
497+
}
498+
}
499+
}
500+
501+
// Copy any non-processed files into the output locations
502+
const outputPaths = [...i18n.inlineLocales].map(l =>
503+
path.join(baseOutputPath, l),
504+
);
505+
await copyAssets(
506+
[
507+
{
508+
glob: '**/*',
509+
// tslint:disable-next-line: no-non-null-assertion
510+
input: webpackStats.outputPath!,
511+
output: '',
512+
ignore: [...processedFiles].map(f =>
513+
// tslint:disable-next-line: no-non-null-assertion
514+
path.relative(webpackStats.outputPath!, f),
515+
),
516+
},
517+
],
518+
outputPaths,
519+
'',
520+
);
493521
} catch (err) {
494522
context.logger.error('Localized bundle generation failed: ' + err.message);
495523

496524
return { success: false };
497-
} finally {
498-
try {
499-
// Remove temporary directory used for i18n processing
500-
// tslint:disable-next-line: no-non-null-assertion
501-
await host.delete(normalize(webpackStats.outputPath!)).toPromise();
502-
} catch {}
503525
}
504-
}
505526

506-
context.logger.info(
507-
`Localized bundle generation ${diagnostics.hasErrors ? 'failed' : 'complete'}.`,
508-
);
527+
context.logger.info(
528+
`Localized bundle generation ${hasErrors ? 'failed' : 'complete'}.`,
529+
);
509530

510-
for (const message of diagnostics.messages) {
511-
if (message.type === 'error') {
512-
context.logger.error(message.message);
513-
} else {
514-
context.logger.warn(message.message);
531+
if (hasErrors) {
532+
return { success: false };
515533
}
516534
}
535+
} finally {
536+
executor.stop();
517537

518-
if (diagnostics.hasErrors) {
519-
return { success: false };
538+
if (i18n.shouldInline) {
539+
try {
540+
// Remove temporary directory used for i18n processing
541+
// tslint:disable-next-line: no-non-null-assertion
542+
await host.delete(normalize(webpackStats.outputPath!)).toPromise();
543+
} catch {}
520544
}
521545
}
522546

packages/angular_devkit/build_angular/src/utils/load-translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function createTranslationLoader(): Promise<TranslationLoader> {
3030

3131
for (const [format, parser] of Object.entries(parsers)) {
3232
if (parser.canParse(path, content)) {
33-
return { format, translation: parser.parse(path, content) };
33+
return { format, translation: parser.parse(path, content).translations };
3434
}
3535
}
3636

0 commit comments

Comments
 (0)