Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): improve quality of localized sour…
Browse files Browse the repository at this point in the history
…cemaps

Fixes: #17131
(cherry picked from commit 2e84203)
  • Loading branch information
clydin authored and dgp1130 committed Mar 24, 2020
1 parent 79cd542 commit 7f4da00
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 36 deletions.
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"less-loader": "5.0.0",
"license-webpack-plugin": "2.1.4",
"loader-utils": "2.0.0",
"magic-string": "0.25.7",
"mini-css-extract-plugin": "0.9.0",
"minimatch": "3.0.4",
"parse5": "4.0.0",
Expand Down
72 changes: 40 additions & 32 deletions packages/angular_devkit/build_angular/src/utils/process-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import * as path from 'path';
import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
import { minify } from 'terser';
import * as v8 from 'v8';
import { SourceMapSource } from 'webpack-sources';
import {
ConcatSource,
OriginalSource,
ReplaceSource,
Source,
SourceMapSource,
} from 'webpack-sources';
import { allowMangle, allowMinify, shouldBeautify } from './environment-options';
import { I18nOptions } from './i18n-options';

Expand All @@ -48,7 +54,7 @@ export interface ProcessBundleOptions {
integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512';
runtimeData?: ProcessBundleResult[];
replacements?: [string, string][];
supportedBrowsers?: string [] | Record<string, string>;
supportedBrowsers?: string[] | Record<string, string>;
}

export interface ProcessBundleResult {
Expand Down Expand Up @@ -665,7 +671,7 @@ export async function inlineLocales(options: InlineOptions) {
fs.writeFileSync(outputPath, transformResult.code);

if (inputMap && transformResult.map) {
const outputMap = mergeSourceMaps(
const outputMap = await mergeSourceMaps(
options.code,
inputMap,
transformResult.code,
Expand All @@ -686,7 +692,6 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
return { file: options.filename, diagnostics: [], count: 0 };
}

const { default: MagicString } = await import('magic-string');
const { default: generate } = await import('@babel/generator');
const utils = await import(
// tslint:disable-next-line: trailing-comma no-implicit-dependencies
Expand All @@ -702,11 +707,21 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
return inlineCopyOnly(options);
}

// tslint:disable-next-line: no-any
let content = new MagicString(options.code, { filename: options.filename } as any);
const inputMap = options.map && (JSON.parse(options.map) as RawSourceMap);
let contentClone;
// Cleanup source root otherwise it will be added to each source entry
const mapSourceRoot = inputMap && inputMap.sourceRoot;
if (inputMap) {
delete inputMap.sourceRoot;
}

for (const locale of i18n.inlineLocales) {
const content = new ReplaceSource(
inputMap
? // tslint:disable-next-line: no-any
new SourceMapSource(options.code, options.filename, inputMap as any)
: new OriginalSource(options.code, options.filename),
);

const isSourceLocale = locale === i18n.sourceLocale;
// tslint:disable-next-line: no-any
const translations: any = isSourceLocale ? {} : i18n.locales[locale].translation || {};
Expand All @@ -722,49 +737,42 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
const expression = utils.buildLocalizeReplacement(translated[0], translated[1]);
const { code } = generate(expression);

content.overwrite(position.start, position.end, code);
content.replace(position.start, position.end - 1, code);
}

let outputSource: Source = content;
if (options.setLocale) {
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});`;
contentClone = content.clone();
content.prepend(setLocaleText);
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});\n`;

// If locale data is provided, load it and prepend to file
let localeDataSource: Source | null = null;
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
if (localeDataPath) {
const localDataContent = await loadLocaleData(localeDataPath, true);
// The semicolon ensures that there is no syntax error between statements
content.prepend(localDataContent + ';');
const localeDataContent = await loadLocaleData(localeDataPath, true);
localeDataSource = new OriginalSource(localeDataContent, path.basename(localeDataPath));
}

outputSource = localeDataSource
// The semicolon ensures that there is no syntax error between statements
? new ConcatSource(setLocaleText, localeDataSource, ';\n', content)
: new ConcatSource(setLocaleText, content);
}

const output = content.toString();
const { source: outputCode, map: outputMap } = outputSource.sourceAndMap();
const outputPath = path.join(
options.outputPath,
i18n.flatOutput ? '' : locale,
options.filename,
);
fs.writeFileSync(outputPath, output);

if (inputMap) {
const contentMap = content.generateMap();
const outputMap = mergeSourceMaps(
options.code,
inputMap,
output,
contentMap,
options.filename,
options.code.length > FAST_SOURCEMAP_THRESHOLD,
);
fs.writeFileSync(outputPath, outputCode);

if (inputMap && outputMap) {
outputMap.file = options.filename;
if (mapSourceRoot) {
outputMap.sourceRoot = mapSourceRoot;
}
fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap));
}

if (contentClone) {
content = contentClone;
contentClone = undefined;
}
}

return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length };
Expand Down
14 changes: 12 additions & 2 deletions tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expectFileNotToExist, expectFileToMatch, writeFile } from '../../utils/fs';
import { expectFileNotToExist, expectFileToMatch, readFile, writeFile } from '../../utils/fs';
import { ng } from '../../utils/process';
import { updateJsonFile } from '../../utils/project';
import { expectToFail } from '../../utils/utils';
Expand All @@ -15,12 +15,22 @@ export default async function() {
config.angularCompilerOptions.disableTypeScriptVersionCheck = true;
});

await ng('build');
await ng('build', '--source-map');
for (const { lang, outputPath, translation } of langTranslations) {
await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial);
await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`'));
await expectFileNotToExist(`${outputPath}/main-es5.js`);

// Ensure sourcemap for modified file contains content
const mainSourceMap = JSON.parse(await readFile(`${outputPath}/main.js.map`));
if (
mainSourceMap.version !== 3 ||
!Array.isArray(mainSourceMap.sources) ||
typeof mainSourceMap.mappings !== 'string'
) {
throw new Error('invalid localized sourcemap for main.js');
}

// Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references)
// The only reference in a new application is in @angular/core
await expectFileToMatch(`${outputPath}/vendor.js`, lang);
Expand Down
12 changes: 11 additions & 1 deletion tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expectFileNotToExist, expectFileToMatch } from '../../utils/fs';
import { expectFileNotToExist, expectFileToMatch, readFile } from '../../utils/fs';
import { ng } from '../../utils/process';
import { updateJsonFile } from '../../utils/project';
import { expectToFail } from '../../utils/utils';
Expand All @@ -21,6 +21,16 @@ export default async function() {
await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`'));
await expectFileNotToExist(`${outputPath}/main-es2015.js`);

// Ensure sourcemap for modified file contains content
const mainSourceMap = JSON.parse(await readFile(`${outputPath}/main.js.map`));
if (
mainSourceMap.version !== 3 ||
!Array.isArray(mainSourceMap.sources) ||
typeof mainSourceMap.mappings !== 'string'
) {
throw new Error('invalid localized sourcemap for main.js');
}

// Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references)
// The only reference in a new application is in @angular/core
await expectFileToMatch(`${outputPath}/vendor.js`, lang);
Expand Down

0 comments on commit 7f4da00

Please sign in to comment.