diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 6caf622074c2..f3e32ec87d2e 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -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", diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 58e3b7ce7686..27267a2a2a38 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -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'; @@ -48,7 +54,7 @@ export interface ProcessBundleOptions { integrityAlgorithm?: 'sha256' | 'sha384' | 'sha512'; runtimeData?: ProcessBundleResult[]; replacements?: [string, string][]; - supportedBrowsers?: string [] | Record; + supportedBrowsers?: string[] | Record; } export interface ProcessBundleResult { @@ -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, @@ -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 @@ -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 || {}; @@ -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 }; diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts index 50c3971e0643..31b8f3f90f27 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts @@ -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'; @@ -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); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts index 12d33c0bc429..e549ae616f55 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts @@ -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'; @@ -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);