Skip to content

fix(@angular-devkit/build-angular): improve quality of localized source maps #17266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 24, 2020
Merged
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
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