Skip to content

Commit 5bd3b91

Browse files
clydinKeen Yee Liau
authored and
Keen Yee Liau
committed
refactor(@angular-devkit/build-angular): use localize babel plugins directly
With recent improvements in the performance of babel parsing and AST traversal, the localize babel plugins can now be leveraged directly.
1 parent 8543bf7 commit 5bd3b91

File tree

5 files changed

+120
-144
lines changed

5 files changed

+120
-144
lines changed

packages/angular_devkit/build_angular/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"@angular-devkit/build-webpack": "0.0.0",
1313
"@angular-devkit/core": "0.0.0",
1414
"@babel/core": "7.8.4",
15-
"@babel/generator": "7.8.4",
1615
"@babel/preset-env": "7.8.4",
16+
"@babel/template": "7.8.3",
1717
"@ngtools/webpack": "0.0.0",
1818
"ajv": "6.11.0",
1919
"autoprefixer": "9.7.4",
@@ -35,7 +35,6 @@
3535
"less-loader": "5.0.0",
3636
"license-webpack-plugin": "2.1.3",
3737
"loader-utils": "1.2.3",
38-
"magic-string": "0.25.6",
3938
"mini-css-extract-plugin": "0.9.0",
4039
"minimatch": "3.0.4",
4140
"parse5": "4.0.0",

packages/angular_devkit/build_angular/src/utils/process-bundle.ts

+109-138
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
PluginObj,
1212
parseSync,
1313
transformAsync,
14-
traverse,
14+
transformFromAstSync,
1515
types,
1616
} from '@babel/core';
17+
import templateBuilder from '@babel/template';
1718
import { createHash } from 'crypto';
1819
import * as fs from 'fs';
1920
import * as path from 'path';
@@ -490,6 +491,59 @@ function createReplacePlugin(replacements: [string, string][]): PluginObj {
490491
};
491492
}
492493

494+
async function createI18nPlugins(
495+
locale: string,
496+
translation: unknown | undefined,
497+
missingTranslation: 'error' | 'warning' | 'ignore',
498+
localeDataContent: string | undefined,
499+
) {
500+
const plugins = [];
501+
// tslint:disable-next-line: no-implicit-dependencies
502+
const localizeDiag = await import('@angular/localize/src/tools/src/diagnostics');
503+
504+
const diagnostics = new localizeDiag.Diagnostics();
505+
506+
const es2015 = await import(
507+
// tslint:disable-next-line: trailing-comma no-implicit-dependencies
508+
'@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin'
509+
);
510+
plugins.push(
511+
// tslint:disable-next-line: no-any
512+
es2015.makeEs2015TranslatePlugin(diagnostics, (translation || {}) as any, {
513+
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
514+
}),
515+
);
516+
517+
const es5 = await import(
518+
// tslint:disable-next-line: trailing-comma no-implicit-dependencies
519+
'@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin'
520+
);
521+
plugins.push(
522+
// tslint:disable-next-line: no-any
523+
es5.makeEs5TranslatePlugin(diagnostics, (translation || {}) as any, {
524+
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
525+
}),
526+
);
527+
528+
const inlineLocale = await import(
529+
// tslint:disable-next-line: trailing-comma no-implicit-dependencies
530+
'@angular/localize/src/tools/src/translate/source_files/locale_plugin'
531+
);
532+
plugins.push(inlineLocale.makeLocalePlugin(locale));
533+
534+
if (localeDataContent) {
535+
plugins.push({
536+
visitor: {
537+
Program(path: NodePath<types.Program>) {
538+
path.unshiftContainer('body', templateBuilder.ast(localeDataContent));
539+
},
540+
},
541+
});
542+
}
543+
544+
return { diagnostics, plugins };
545+
}
546+
493547
export interface InlineOptions {
494548
filename: string;
495549
code: string;
@@ -500,13 +554,6 @@ export interface InlineOptions {
500554
setLocale?: boolean;
501555
}
502556

503-
interface LocalizePosition {
504-
start: number;
505-
end: number;
506-
messageParts: TemplateStringsArray;
507-
expressions: types.Expression[];
508-
}
509-
510557
const localizeName = '$localize';
511558

512559
export async function inlineLocales(options: InlineOptions) {
@@ -522,88 +569,91 @@ export async function inlineLocales(options: InlineOptions) {
522569
return inlineCopyOnly(options);
523570
}
524571

525-
const { default: MagicString } = await import('magic-string');
526-
const { default: generate } = await import('@babel/generator');
527-
const utils = await import(
528-
// tslint:disable-next-line: trailing-comma no-implicit-dependencies
529-
'@angular/localize/src/tools/src/translate/source_files/source_file_utils'
530-
);
531-
// tslint:disable-next-line: no-implicit-dependencies
532-
const localizeDiag = await import('@angular/localize/src/tools/src/diagnostics');
533-
534-
const diagnostics = new localizeDiag.Diagnostics();
572+
let ast: ParseResult | undefined | null;
573+
try {
574+
ast = parseSync(options.code, {
575+
babelrc: false,
576+
configFile: false,
577+
sourceType: 'script',
578+
filename: options.filename,
579+
});
580+
} catch (error) {
581+
if (error.message) {
582+
// Make the error more readable.
583+
// Same errors will contain the full content of the file as the error message
584+
// Which makes it hard to find the actual error message.
585+
const index = error.message.indexOf(')\n');
586+
const msg = index !== -1 ? error.message.substr(0, index + 1) : error.message;
587+
throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`);
588+
}
589+
}
535590

536-
const positions = findLocalizePositions(options, utils);
537-
if (positions.length === 0 && !options.setLocale) {
538-
return inlineCopyOnly(options);
591+
if (!ast) {
592+
throw new Error(`Unknown error occurred inlining file "${options.filename}"`);
539593
}
540594

541-
// tslint:disable-next-line: no-any
542-
let content = new MagicString(options.code, { filename: options.filename } as any);
595+
const diagnostics = [];
543596
const inputMap = options.map && (JSON.parse(options.map) as RawSourceMap);
544-
let contentClone;
545597
for (const locale of i18n.inlineLocales) {
546598
const isSourceLocale = locale === i18n.sourceLocale;
547599
// tslint:disable-next-line: no-any
548600
const translations: any = isSourceLocale ? {} : i18n.locales[locale].translation || {};
549-
for (const position of positions) {
550-
const translated = utils.translate(
551-
diagnostics,
552-
translations,
553-
position.messageParts,
554-
position.expressions,
555-
isSourceLocale ? 'ignore' : options.missingTranslation || 'warning',
556-
);
557-
558-
const expression = utils.buildLocalizeReplacement(translated[0], translated[1]);
559-
const { code } = generate(expression);
560-
561-
content.overwrite(position.start, position.end, code);
562-
}
563-
601+
let localeDataContent;
564602
if (options.setLocale) {
565-
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});`;
566-
contentClone = content.clone();
567-
content.prepend(setLocaleText);
568-
569603
// If locale data is provided, load it and prepend to file
570-
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
604+
const localeDataPath = i18n.locales[locale]?.dataPath;
571605
if (localeDataPath) {
572-
const localDataContent = await loadLocaleData(localeDataPath, true);
573-
// The semicolon ensures that there is no syntax error between statements
574-
content.prepend(localDataContent + ';');
606+
localeDataContent = await loadLocaleData(localeDataPath, true);
575607
}
576608
}
577609

578-
const output = content.toString();
610+
const { diagnostics: localeDiagnostics, plugins } = await createI18nPlugins(
611+
locale,
612+
translations,
613+
isSourceLocale ? 'ignore' : options.missingTranslation || 'warning',
614+
localeDataContent,
615+
);
616+
const transformResult = await transformFromAstSync(ast, options.code, {
617+
filename: options.filename,
618+
// using false ensures that babel will NOT search and process sourcemap comments (large memory usage)
619+
// The types do not include the false option even though it is valid
620+
// tslint:disable-next-line: no-any
621+
inputSourceMap: false as any,
622+
babelrc: false,
623+
configFile: false,
624+
plugins,
625+
compact: !shouldBeautify,
626+
sourceMaps: !!inputMap,
627+
});
628+
629+
diagnostics.push(...localeDiagnostics.messages);
630+
631+
if (!transformResult || !transformResult.code) {
632+
throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`);
633+
}
634+
579635
const outputPath = path.join(
580636
options.outputPath,
581637
i18n.flatOutput ? '' : locale,
582638
options.filename,
583639
);
584-
fs.writeFileSync(outputPath, output);
640+
fs.writeFileSync(outputPath, transformResult.code);
585641

586-
if (inputMap) {
587-
const contentMap = content.generateMap();
642+
if (inputMap && transformResult.map) {
588643
const outputMap = mergeSourceMaps(
589644
options.code,
590645
inputMap,
591-
output,
592-
contentMap,
646+
transformResult.code,
647+
transformResult.map,
593648
options.filename,
594649
options.code.length > FAST_SOURCEMAP_THRESHOLD,
595650
);
596651

597652
fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap));
598653
}
599-
600-
if (contentClone) {
601-
content = contentClone;
602-
contentClone = undefined;
603-
}
604654
}
605655

606-
return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length };
656+
return { file: options.filename, diagnostics };
607657
}
608658

609659
function inlineCopyOnly(options: InlineOptions) {
@@ -626,85 +676,6 @@ function inlineCopyOnly(options: InlineOptions) {
626676
return { file: options.filename, diagnostics: [], count: 0 };
627677
}
628678

629-
function findLocalizePositions(
630-
options: InlineOptions,
631-
// tslint:disable-next-line: no-implicit-dependencies
632-
utils: typeof import('@angular/localize/src/tools/src/translate/source_files/source_file_utils'),
633-
): LocalizePosition[] {
634-
let ast: ParseResult | undefined | null;
635-
636-
try {
637-
ast = parseSync(options.code, {
638-
babelrc: false,
639-
configFile: false,
640-
sourceType: 'script',
641-
filename: options.filename,
642-
});
643-
} catch (error) {
644-
if (error.message) {
645-
// Make the error more readable.
646-
// Same errors will contain the full content of the file as the error message
647-
// Which makes it hard to find the actual error message.
648-
const index = error.message.indexOf(')\n');
649-
const msg = index !== -1 ? error.message.substr(0, index + 1) : error.message;
650-
throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`);
651-
}
652-
}
653-
654-
if (!ast) {
655-
throw new Error(`Unknown error occurred inlining file "${options.filename}"`);
656-
}
657-
658-
const positions: LocalizePosition[] = [];
659-
if (options.es5) {
660-
traverse(ast, {
661-
CallExpression(path: NodePath<types.CallExpression>) {
662-
const callee = path.get('callee');
663-
if (
664-
callee.isIdentifier() &&
665-
callee.node.name === localizeName &&
666-
utils.isGlobalIdentifier(callee)
667-
) {
668-
const messageParts = utils.unwrapMessagePartsFromLocalizeCall(path);
669-
const expressions = utils.unwrapSubstitutionsFromLocalizeCall(path.node);
670-
positions.push({
671-
// tslint:disable-next-line: no-non-null-assertion
672-
start: path.node.start!,
673-
// tslint:disable-next-line: no-non-null-assertion
674-
end: path.node.end!,
675-
messageParts,
676-
expressions,
677-
});
678-
}
679-
},
680-
});
681-
} else {
682-
const traverseFast = ((types as unknown) as {
683-
traverseFast: (node: types.Node, enter: (node: types.Node) => void) => void;
684-
}).traverseFast;
685-
686-
traverseFast(ast, node => {
687-
if (
688-
node.type === 'TaggedTemplateExpression' &&
689-
types.isIdentifier(node.tag) &&
690-
node.tag.name === localizeName
691-
) {
692-
const messageParts = utils.unwrapMessagePartsFromTemplateLiteral(node.quasi.quasis);
693-
positions.push({
694-
// tslint:disable-next-line: no-non-null-assertion
695-
start: node.start!,
696-
// tslint:disable-next-line: no-non-null-assertion
697-
end: node.end!,
698-
messageParts,
699-
expressions: node.quasi.expressions,
700-
});
701-
}
702-
});
703-
}
704-
705-
return positions;
706-
}
707-
708679
async function loadLocaleData(path: string, optimize: boolean): Promise<string> {
709680
// The path is validated during option processing before the build starts
710681
const content = fs.readFileSync(path, 'utf8');

tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export default async function() {
2020
await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial);
2121
await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`'));
2222
await expectFileNotToExist(`${outputPath}/main-es5.js`);
23-
await expectFileToMatch(`${outputPath}/main.js`, lang);
23+
24+
// Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references)
25+
// The only reference in a new application is in @angular/core
26+
await expectFileToMatch(`${outputPath}/vendor.js`, lang);
2427

2528
// Verify the HTML lang attribute is present
2629
await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`);

tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export default async function() {
2020
await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial);
2121
await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`'));
2222
await expectFileNotToExist(`${outputPath}/main-es2015.js`);
23-
await expectFileToMatch(`${outputPath}/main.js`, lang);
23+
24+
// Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references)
25+
// The only reference in a new application is in @angular/core
26+
await expectFileToMatch(`${outputPath}/vendor.js`, lang);
2427

2528
// Verify the HTML lang attribute is present
2629
await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`);

yarn.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
semver "^5.4.1"
152152
source-map "^0.5.0"
153153

154-
"@babel/generator@7.8.4", "@babel/generator@^7.4.0", "@babel/generator@^7.8.3", "@babel/generator@^7.8.4":
154+
"@babel/generator@^7.4.0", "@babel/generator@^7.8.3", "@babel/generator@^7.8.4":
155155
version "7.8.4"
156156
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e"
157157
integrity sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==
@@ -794,7 +794,7 @@
794794
dependencies:
795795
regenerator-runtime "^0.13.2"
796796

797-
"@babel/template@^7.4.0", "@babel/template@^7.7.4", "@babel/template@^7.8.3":
797+
"@babel/template@7.8.3", "@babel/template@^7.4.0", "@babel/template@^7.7.4", "@babel/template@^7.8.3":
798798
version "7.8.3"
799799
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
800800
integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==

0 commit comments

Comments
 (0)