From 79620f5139c80f1b3b38168744ca99a224c7e5cd Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sat, 26 Sep 2020 14:29:55 +0100 Subject: [PATCH] refactor(localize): avoid free-standing `FileSystem` functions (#39006) These free standing functions rely upon the "current" `FileSystem`, but it is safer to explicitly pass the `FileSystem` into functions or classes that need it. Fixes #38711 PR Close #39006 --- .../src/tools/src/extract/extraction.ts | 4 +- .../localize/src/tools/src/extract/main.ts | 27 +++++---- .../source_files/es2015_extract_plugin.ts | 9 +-- .../source_files/es5_extract_plugin.ts | 11 ++-- .../xliff1_translation_serializer.ts | 6 +- .../xliff2_translation_serializer.ts | 6 +- .../xmb_translation_serializer.ts | 9 ++- .../src/tools/src/source_file_utils.ts | 58 ++++++++++++++----- .../source_files/es2015_translate_plugin.ts | 7 ++- .../source_files/es5_translate_plugin.ts | 9 +-- .../source_file_translation_handler.ts | 4 +- .../test/extract/integration/main_spec.ts | 16 ++++- .../xmb_translation_serializer_spec.ts | 7 ++- .../src/tools/test/source_file_utils_spec.ts | 27 +++++---- .../es2015_translate_plugin_spec.ts | 21 ++++--- .../source_files/es5_translate_plugin_spec.ts | 21 ++++--- 16 files changed, 155 insertions(+), 87 deletions(-) diff --git a/packages/localize/src/tools/src/extract/extraction.ts b/packages/localize/src/tools/src/extract/extraction.ts index 9307a1090c605..ae1644e31a5a6 100644 --- a/packages/localize/src/tools/src/extract/extraction.ts +++ b/packages/localize/src/tools/src/extract/extraction.ts @@ -52,8 +52,8 @@ export class MessageExtractor { sourceRoot: this.basePath, filename, plugins: [ - makeEs2015ExtractPlugin(messages, this.localizeName), - makeEs5ExtractPlugin(messages, this.localizeName), + makeEs2015ExtractPlugin(this.fs, messages, this.localizeName), + makeEs5ExtractPlugin(this.fs, messages, this.localizeName), ], code: false, ast: false diff --git a/packages/localize/src/tools/src/extract/main.ts b/packages/localize/src/tools/src/extract/main.ts index 646fab708e8d5..899a994a42ce7 100644 --- a/packages/localize/src/tools/src/extract/main.ts +++ b/packages/localize/src/tools/src/extract/main.ts @@ -6,13 +6,13 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {getFileSystem, setFileSystem, NodeJSFileSystem, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {setFileSystem, NodeJSFileSystem, AbsoluteFsPath, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/src/ngtsc/logging'; import {ɵParsedMessage} from '@angular/localize'; import * as glob from 'glob'; import * as yargs from 'yargs'; -import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics'; +import {DiagnosticHandlingStrategy} from '../diagnostics'; import {checkDuplicateMessages} from './duplicates'; import {MessageExtractor} from './extraction'; @@ -97,8 +97,8 @@ if (require.main === module) { .help() .parse(args); - const fs = new NodeJSFileSystem(); - setFileSystem(fs); + const fileSystem = new NodeJSFileSystem(); + setFileSystem(fileSystem); const rootPath = options.r; const sourceFilePaths = glob.sync(options.s, {cwd: rootPath, nodir: true}); @@ -119,6 +119,7 @@ if (require.main === module) { useLegacyIds: options.useLegacyIds, duplicateMessageHandling, formatOptions, + fileSystem, }); } @@ -166,6 +167,10 @@ export interface ExtractTranslationsOptions { * A collection of formatting options to pass to the translation file serializer. */ formatOptions?: FormatOptions; + /** + * The file-system abstraction to use. + */ + fileSystem: FileSystem; } export function extractTranslations({ @@ -179,8 +184,8 @@ export function extractTranslations({ useLegacyIds, duplicateMessageHandling, formatOptions = {}, + fileSystem: fs, }: ExtractTranslationsOptions) { - const fs = getFileSystem(); const basePath = fs.resolve(rootPath); const extractor = new MessageExtractor(fs, logger, {basePath, useSourceMaps}); @@ -196,7 +201,7 @@ export function extractTranslations({ const outputPath = fs.resolve(rootPath, output); const serializer = - getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions); + getSerializer(format, sourceLocale, fs.dirname(outputPath), useLegacyIds, formatOptions, fs); const translationFile = serializer.serialize(messages); fs.ensureDir(fs.dirname(outputPath)); fs.writeFile(outputPath, translationFile); @@ -208,18 +213,20 @@ export function extractTranslations({ export function getSerializer( format: string, sourceLocale: string, rootPath: AbsoluteFsPath, useLegacyIds: boolean, - formatOptions: FormatOptions = {}): TranslationSerializer { + formatOptions: FormatOptions = {}, fs: FileSystem): TranslationSerializer { switch (format) { case 'xlf': case 'xlif': case 'xliff': - return new Xliff1TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions); + return new Xliff1TranslationSerializer( + sourceLocale, rootPath, useLegacyIds, formatOptions, fs); case 'xlf2': case 'xlif2': case 'xliff2': - return new Xliff2TranslationSerializer(sourceLocale, rootPath, useLegacyIds, formatOptions); + return new Xliff2TranslationSerializer( + sourceLocale, rootPath, useLegacyIds, formatOptions, fs); case 'xmb': - return new XmbTranslationSerializer(rootPath, useLegacyIds); + return new XmbTranslationSerializer(rootPath, useLegacyIds, fs); case 'json': return new SimpleJsonTranslationSerializer(sourceLocale); } diff --git a/packages/localize/src/tools/src/extract/source_files/es2015_extract_plugin.ts b/packages/localize/src/tools/src/extract/source_files/es2015_extract_plugin.ts index 9c4efee6aef87..646a77b285a77 100644 --- a/packages/localize/src/tools/src/extract/source_files/es2015_extract_plugin.ts +++ b/packages/localize/src/tools/src/extract/source_files/es2015_extract_plugin.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedMessage, ɵparseMessage} from '@angular/localize'; import {NodePath, PluginObj} from '@babel/core'; import {TaggedTemplateExpression} from '@babel/types'; @@ -12,7 +13,7 @@ import {TaggedTemplateExpression} from '@babel/types'; import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapExpressionsFromTemplateLiteral, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils'; export function makeEs2015ExtractPlugin( - messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj { + fs: FileSystem, messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj { return { visitor: { TaggedTemplateExpression(path: NodePath) { @@ -20,10 +21,10 @@ export function makeEs2015ExtractPlugin( if (isNamedIdentifier(tag, localizeName) && isGlobalIdentifier(tag)) { const quasiPath = path.get('quasi'); const [messageParts, messagePartLocations] = - unwrapMessagePartsFromTemplateLiteral(quasiPath.get('quasis')); + unwrapMessagePartsFromTemplateLiteral(quasiPath.get('quasis'), fs); const [expressions, expressionLocations] = - unwrapExpressionsFromTemplateLiteral(quasiPath); - const location = getLocation(quasiPath); + unwrapExpressionsFromTemplateLiteral(quasiPath, fs); + const location = getLocation(fs, quasiPath); const message = ɵparseMessage( messageParts, expressions, location, messagePartLocations, expressionLocations); messages.push(message); diff --git a/packages/localize/src/tools/src/extract/source_files/es5_extract_plugin.ts b/packages/localize/src/tools/src/extract/source_files/es5_extract_plugin.ts index 1814114c9c0a1..fc83d4ca40400 100644 --- a/packages/localize/src/tools/src/extract/source_files/es5_extract_plugin.ts +++ b/packages/localize/src/tools/src/extract/source_files/es5_extract_plugin.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedMessage, ɵparseMessage} from '@angular/localize'; import {NodePath, PluginObj} from '@babel/core'; import {CallExpression} from '@babel/types'; @@ -12,16 +13,18 @@ import {CallExpression} from '@babel/types'; import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapMessagePartsFromLocalizeCall, unwrapSubstitutionsFromLocalizeCall} from '../../source_file_utils'; export function makeEs5ExtractPlugin( - messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj { + fs: FileSystem, messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj { return { visitor: { CallExpression(callPath: NodePath) { const calleePath = callPath.get('callee'); if (isNamedIdentifier(calleePath, localizeName) && isGlobalIdentifier(calleePath)) { - const [messageParts, messagePartLocations] = unwrapMessagePartsFromLocalizeCall(callPath); - const [expressions, expressionLocations] = unwrapSubstitutionsFromLocalizeCall(callPath); + const [messageParts, messagePartLocations] = + unwrapMessagePartsFromLocalizeCall(callPath, fs); + const [expressions, expressionLocations] = + unwrapSubstitutionsFromLocalizeCall(callPath, fs); const [messagePartsArg, expressionsArg] = callPath.get('arguments'); - const location = getLocation(messagePartsArg, expressionsArg); + const location = getLocation(fs, messagePartsArg, expressionsArg); const message = ɵparseMessage( messageParts, expressions, location, messagePartLocations, expressionLocations); messages.push(message); diff --git a/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts index 59b2b79b658bd..363000a557e59 100644 --- a/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts +++ b/packages/localize/src/tools/src/extract/translation_files/xliff1_translation_serializer.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {FormatOptions, validateOptions} from './format_options'; @@ -28,7 +28,7 @@ const LEGACY_XLIFF_MESSAGE_LENGTH = 40; export class Xliff1TranslationSerializer implements TranslationSerializer { constructor( private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean, - private formatOptions: FormatOptions = {}) { + private formatOptions: FormatOptions = {}, private fs: FileSystem = getFileSystem()) { validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions); } @@ -115,7 +115,7 @@ export class Xliff1TranslationSerializer implements TranslationSerializer { private serializeLocation(xml: XmlFile, location: ɵSourceLocation): void { xml.startTag('context-group', {purpose: 'location'}); - this.renderContext(xml, 'sourcefile', relative(this.basePath, location.file)); + this.renderContext(xml, 'sourcefile', this.fs.relative(this.basePath, location.file)); const endLineString = location.end !== undefined && location.end.line !== location.start.line ? `,${location.end.line + 1}` : ''; diff --git a/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts index fc8bc82e21016..177ea75e69f4c 100644 --- a/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts +++ b/packages/localize/src/tools/src/extract/translation_files/xliff2_translation_serializer.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {FormatOptions, validateOptions} from './format_options'; @@ -28,7 +28,7 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { private currentPlaceholderId = 0; constructor( private sourceLocale: string, private basePath: AbsoluteFsPath, private useLegacyIds: boolean, - private formatOptions: FormatOptions = {}) { + private formatOptions: FormatOptions = {}, private fs: FileSystem = getFileSystem()) { validateOptions('Xliff1TranslationSerializer', [['xml:space', ['preserve']]], formatOptions); } @@ -64,7 +64,7 @@ export class Xliff2TranslationSerializer implements TranslationSerializer { end !== undefined && end.line !== start.line ? `,${end.line + 1}` : ''; this.serializeNote( xml, 'location', - `${relative(this.basePath, file)}:${start.line + 1}${endLineString}`); + `${this.fs.relative(this.basePath, file)}:${start.line + 1}${endLineString}`); } if (message.description) { this.serializeNote(xml, 'description', message.description); diff --git a/packages/localize/src/tools/src/extract/translation_files/xmb_translation_serializer.ts b/packages/localize/src/tools/src/extract/translation_files/xmb_translation_serializer.ts index 6f8238840e67c..04d2d6556c643 100644 --- a/packages/localize/src/tools/src/extract/translation_files/xmb_translation_serializer.ts +++ b/packages/localize/src/tools/src/extract/translation_files/xmb_translation_serializer.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, relative} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; import {extractIcuPlaceholders} from './icu_parsing'; @@ -21,7 +21,9 @@ import {XmlFile} from './xml_file'; * @publicApi used by CLI */ export class XmbTranslationSerializer implements TranslationSerializer { - constructor(private basePath: AbsoluteFsPath, private useLegacyIds: boolean) {} + constructor( + private basePath: AbsoluteFsPath, private useLegacyIds: boolean, + private fs: FileSystem = getFileSystem()) {} serialize(messages: ɵParsedMessage[]): string { const ids = new Set(); @@ -74,7 +76,8 @@ export class XmbTranslationSerializer implements TranslationSerializer { const endLineString = location.end !== undefined && location.end.line !== location.start.line ? `,${location.end.line + 1}` : ''; - xml.text(`${relative(this.basePath, location.file)}:${location.start.line}${endLineString}`); + xml.text( + `${this.fs.relative(this.basePath, location.file)}:${location.start.line}${endLineString}`); xml.endTag('source'); } diff --git a/packages/localize/src/tools/src/source_file_utils.ts b/packages/localize/src/tools/src/source_file_utils.ts index 4db7179a10173..9ba2b85837d12 100644 --- a/packages/localize/src/tools/src/source_file_utils.ts +++ b/packages/localize/src/tools/src/source_file_utils.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, relative, resolve} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵisMissingTranslationError, ɵmakeTemplateObject, ɵParsedTranslation, ɵSourceLocation, ɵtranslate} from '@angular/localize'; import {NodePath} from '@babel/traverse'; import * as t from '@babel/types'; @@ -68,10 +68,14 @@ export function buildLocalizeReplacement( * to a helper function like `__makeTemplateObject`. * * @param call The AST node of the call to process. + * @param fs The file system to use when computing source-map paths. If not provided then it uses + * the "current" FileSystem. * @publicApi used by CLI */ -export function unwrapMessagePartsFromLocalizeCall(call: NodePath): - [TemplateStringsArray, (ɵSourceLocation | undefined)[]] { +export function unwrapMessagePartsFromLocalizeCall( + call: NodePath, + fs: FileSystem = getFileSystem(), + ): [TemplateStringsArray, (ɵSourceLocation | undefined)[]] { let cooked = call.get('arguments')[0]; if (cooked === undefined) { @@ -141,16 +145,22 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath, + fs: FileSystem = getFileSystem()): [t.Expression[], (ɵSourceLocation | undefined)[]] { const expressions = call.get('arguments').splice(1); if (!isArrayOfExpressions(expressions)) { const badExpression = expressions.find(expression => !expression.isExpression())!; @@ -159,15 +169,21 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath path.node), expressions.map(expression => getLocation(expression)) + expressions.map(path => path.node), expressions.map(expression => getLocation(fs, expression)) ]; } /** * Parse the tagged template literal to extract the message parts. * + * @param elements The elements of the template literal to process. + * @param fs The file system to use when computing source-map paths. If not provided then it uses + * the "current" FileSystem. * @publicApi used by CLI */ +export function unwrapMessagePartsFromTemplateLiteral( + elements: NodePath[], + fs: FileSystem = getFileSystem()): [TemplateStringsArray, (ɵSourceLocation | undefined)[]] { const cooked = elements.map(q => { if (q.node.value.cooked === undefined) { throw new BabelParseError( @@ -177,15 +193,22 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath q.node.value.raw); - const locations = elements.map(q => getLocation(q)); + const locations = elements.map(q => getLocation(fs, q)); return [ɵmakeTemplateObject(cooked, raw), locations]; } /** * Parse the tagged template literal to extract the interpolation expressions. * + * @param quasi The AST node of the template literal to process. + * @param fs The file system to use when computing source-map paths. If not provided then it uses + * the "current" FileSystem. * @publicApi used by CLI */ +export function unwrapExpressionsFromTemplateLiteral( + quasi: NodePath, + fs: FileSystem = getFileSystem()): [t.Expression[], (ɵSourceLocation | undefined)[]] { + return [quasi.node.expressions, quasi.get('expressions').map(e => getLocation(fs, e))]; } /** @@ -205,16 +228,20 @@ export function wrapInParensIfNecessary(expression: t.Expression): t.Expression /** * Extract the string values from an `array` of string literals. + * * @param array The array to unwrap. + * @param fs The file system to use when computing source-map paths. If not provided then it uses + * the "current" FileSystem. */ -export function unwrapStringLiteralArray(array: NodePath): - [string[], (ɵSourceLocation | undefined)[]] { +export function unwrapStringLiteralArray( + array: NodePath, + fs: FileSystem = getFileSystem()): [string[], (ɵSourceLocation | undefined)[]] { if (!isStringLiteralArray(array.node)) { throw new BabelParseError( array.node, 'Unexpected messageParts for `$localize` (expected an array of strings).'); } const elements = array.get('elements') as NodePath[]; - return [elements.map(str => str.node.value), elements.map(str => getLocation(str))]; + return [elements.map(str => str.node.value), elements.map(str => getLocation(fs, str))]; } /** @@ -372,15 +399,16 @@ export function buildCodeFrameError(path: NodePath, e: BabelParseError): string return `${filename}: ${message}`; } -export function getLocation(startPath: NodePath, endPath?: NodePath): ɵSourceLocation|undefined { +export function getLocation( + fs: FileSystem, startPath: NodePath, endPath?: NodePath): ɵSourceLocation|undefined { const startLocation = startPath.node.loc; - const file = getFileFromPath(startPath); + const file = getFileFromPath(fs, startPath); if (!startLocation || !file) { return undefined; } const endLocation = - endPath && getFileFromPath(endPath) === file && endPath.node.loc || startLocation; + endPath && getFileFromPath(fs, endPath) === file && endPath.node.loc || startLocation; return { start: getLineAndColumn(startLocation.start), @@ -397,10 +425,10 @@ export function serializeLocationPosition(location: ɵSourceLocation): string { return `${location.start.line + 1}${endLineString}`; } -function getFileFromPath(path: NodePath|undefined): AbsoluteFsPath|null { +function getFileFromPath(fs: FileSystem, path: NodePath|undefined): AbsoluteFsPath|null { const opts = path?.hub.file.opts; return opts?.filename ? - resolve(opts.generatorOpts.sourceRoot ?? opts.cwd, relative(opts.cwd, opts.filename)) : + fs.resolve(opts.generatorOpts.sourceRoot ?? opts.cwd, fs.relative(opts.cwd, opts.filename)) : null; } diff --git a/packages/localize/src/tools/src/translate/source_files/es2015_translate_plugin.ts b/packages/localize/src/tools/src/translate/source_files/es2015_translate_plugin.ts index 2dc764cfcf408..f2b351f88c8de 100644 --- a/packages/localize/src/tools/src/translate/source_files/es2015_translate_plugin.ts +++ b/packages/localize/src/tools/src/translate/source_files/es2015_translate_plugin.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedTranslation} from '@angular/localize'; import {NodePath, PluginObj} from '@babel/core'; import {TaggedTemplateExpression} from '@babel/types'; @@ -21,8 +22,8 @@ import {buildCodeFrameError, buildLocalizeReplacement, isBabelParseError, isLoca */ export function makeEs2015TranslatePlugin( diagnostics: Diagnostics, translations: Record, - {missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}): - PluginObj { + {missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}, + fs: FileSystem = getFileSystem()): PluginObj { return { visitor: { TaggedTemplateExpression(path: NodePath) { @@ -30,7 +31,7 @@ export function makeEs2015TranslatePlugin( const tag = path.get('tag'); if (isLocalize(tag, localizeName)) { const [messageParts] = - unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis')); + unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis'), fs); const translated = translate( diagnostics, translations, messageParts, path.node.quasi.expressions, missingTranslation); diff --git a/packages/localize/src/tools/src/translate/source_files/es5_translate_plugin.ts b/packages/localize/src/tools/src/translate/source_files/es5_translate_plugin.ts index 8ba02962ef728..33a7066192fc5 100644 --- a/packages/localize/src/tools/src/translate/source_files/es5_translate_plugin.ts +++ b/packages/localize/src/tools/src/translate/source_files/es5_translate_plugin.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {ɵParsedTranslation} from '@angular/localize'; import {NodePath, PluginObj} from '@babel/core'; import {CallExpression} from '@babel/types'; @@ -21,16 +22,16 @@ import {buildCodeFrameError, buildLocalizeReplacement, isBabelParseError, isLoca */ export function makeEs5TranslatePlugin( diagnostics: Diagnostics, translations: Record, - {missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}): - PluginObj { + {missingTranslation = 'error', localizeName = '$localize'}: TranslatePluginOptions = {}, + fs: FileSystem = getFileSystem()): PluginObj { return { visitor: { CallExpression(callPath: NodePath) { try { const calleePath = callPath.get('callee'); if (isLocalize(calleePath, localizeName)) { - const [messageParts] = unwrapMessagePartsFromLocalizeCall(callPath); - const [expressions] = unwrapSubstitutionsFromLocalizeCall(callPath); + const [messageParts] = unwrapMessagePartsFromLocalizeCall(callPath, fs); + const [expressions] = unwrapSubstitutionsFromLocalizeCall(callPath, fs); const translated = translate(diagnostics, translations, messageParts, expressions, missingTranslation); callPath.replaceWith(buildLocalizeReplacement(translated[0], translated[1])); diff --git a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts index 18f41636b4dcc..c3d69b420673f 100644 --- a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts +++ b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts @@ -78,8 +78,8 @@ export class SourceFileTranslationHandler implements TranslationHandler { generatorOpts: {minified: true}, plugins: [ makeLocalePlugin(translationBundle.locale), - makeEs2015TranslatePlugin(diagnostics, translationBundle.translations, options), - makeEs5TranslatePlugin(diagnostics, translationBundle.translations, options), + makeEs2015TranslatePlugin(diagnostics, translationBundle.translations, options, this.fs), + makeEs5TranslatePlugin(diagnostics, translationBundle.translations, options, this.fs), ], filename, }); diff --git a/packages/localize/src/tools/test/extract/integration/main_spec.ts b/packages/localize/src/tools/test/extract/integration/main_spec.ts index d93bb690208f4..797b455818102 100644 --- a/packages/localize/src/tools/test/extract/integration/main_spec.ts +++ b/packages/localize/src/tools/test/extract/integration/main_spec.ts @@ -5,7 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {InvalidFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/src/invalid_file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing'; import {loadTestDirectory} from '@angular/compiler-cli/test/helpers'; @@ -34,6 +35,7 @@ runInEachFileSystem(() => { fs.ensureDir(fs.dirname(sourceFilePath)); loadTestDirectory(fs, __dirname + '/test_files', absoluteFrom('/project/test_files')); + setFileSystem(new InvalidFileSystem()); }); describe('extractTranslations()', () => { @@ -48,6 +50,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds: false, duplicateMessageHandling: 'ignore', + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ `{`, @@ -70,6 +73,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds, duplicateMessageHandling: 'ignore', + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ `{`, @@ -97,6 +101,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds, duplicateMessageHandling: 'ignore', + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ ``, @@ -151,6 +156,7 @@ runInEachFileSystem(() => { useLegacyIds, duplicateMessageHandling: 'ignore', formatOptions, + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ ``, @@ -222,6 +228,7 @@ runInEachFileSystem(() => { useLegacyIds, duplicateMessageHandling: 'ignore', formatOptions, + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ ``, @@ -299,6 +306,7 @@ runInEachFileSystem(() => { useSourceMaps: true, useLegacyIds: false, duplicateMessageHandling: 'ignore', + fileSystem: fs, }); expect(fs.readFile(outputPath)).toEqual([ ``, @@ -308,7 +316,8 @@ runInEachFileSystem(() => { ` `, ` Message in !`, ` `, - // These source file paths are due to how Bazel TypeScript compilation source-maps work + // These source file paths are due to how Bazel TypeScript compilation source-maps + // work ` ../packages/localize/src/tools/test/extract/integration/test_files/src/a.ts`, ` 3`, ` `, @@ -339,6 +348,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds: false, duplicateMessageHandling: 'error', + fileSystem: fs, })) .toThrowError( `Failed to extract messages\n` + @@ -360,6 +370,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds: false, duplicateMessageHandling: 'warning', + fileSystem: fs, }); expect(logger.logs.warn).toEqual([ ['Messages extracted with warnings\n' + @@ -390,6 +401,7 @@ runInEachFileSystem(() => { useSourceMaps: false, useLegacyIds: false, duplicateMessageHandling: 'ignore', + fileSystem: fs, }); expect(logger.logs.warn).toEqual([]); expect(fs.readFile(outputPath)).toEqual([ diff --git a/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts b/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts index f7cf83e9cc11a..89907aa0943ae 100644 --- a/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts +++ b/packages/localize/src/tools/test/extract/translation_files/xmb_translation_serializer_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {absoluteFrom, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; @@ -14,6 +14,8 @@ import {XmbTranslationSerializer} from '../../../src/extract/translation_files/x import {mockMessage} from './mock_message'; runInEachFileSystem(() => { + let fs: FileSystem; + beforeEach(() => fs = getFileSystem()); describe('XmbTranslationSerializer', () => { [false, true].forEach(useLegacyIds => { describe(`renderFile() [using ${useLegacyIds ? 'legacy' : 'canonical'} ids]`, () => { @@ -61,7 +63,8 @@ runInEachFileSystem(() => { ], [], {}), ]; - const serializer = new XmbTranslationSerializer(absoluteFrom('/project'), useLegacyIds); + const serializer = + new XmbTranslationSerializer(absoluteFrom('/project'), useLegacyIds, fs); const output = serializer.serialize(messages); expect(output).toContain([ ``, diff --git a/packages/localize/src/tools/test/source_file_utils_spec.ts b/packages/localize/src/tools/test/source_file_utils_spec.ts index 582c051119e4a..df3db7ede3bc9 100644 --- a/packages/localize/src/tools/test/source_file_utils_spec.ts +++ b/packages/localize/src/tools/test/source_file_utils_spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {absoluteFrom, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {ɵmakeTemplateObject} from '@angular/localize'; import {NodePath, TransformOptions, transformSync} from '@babel/core'; @@ -16,6 +16,8 @@ import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, C import {isGlobalIdentifier, isNamedIdentifier, isStringLiteralArray, isArrayOfExpressions, unwrapStringLiteralArray, unwrapMessagePartsFromLocalizeCall, wrapInParensIfNecessary, buildLocalizeReplacement, unwrapSubstitutionsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral, getLocation} from '../src/source_file_utils'; runInEachFileSystem(() => { + let fs: FileSystem; + beforeEach(() => fs = getFileSystem()); describe('utils', () => { describe('isNamedIdentifier()', () => { it('should return true if the expression is an identifier with name `$localize`', () => { @@ -77,7 +79,7 @@ runInEachFileSystem(() => { it('should return an array of string literals and locations from a direct call to a tag function', () => { const localizeCall = getLocalizeCall(`$localize(['a', 'b\\t', 'c'], 1, 2)`); - const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall); + const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs); expect(parts).toEqual(['a', 'b\t', 'c']); expect(locations).toEqual([ { @@ -105,7 +107,7 @@ runInEachFileSystem(() => { () => { let localizeCall = getLocalizeCall( `$localize(__makeTemplateObject(['a', 'b\\t', 'c'], ['a', 'b\\\\t', 'c']), 1, 2)`); - const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall); + const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs); expect(parts).toEqual(['a', 'b\t', 'c']); expect(parts.raw).toEqual(['a', 'b\\t', 'c']); expect(locations).toEqual([ @@ -138,7 +140,7 @@ runInEachFileSystem(() => { return _templateObject = function() { return e }, e } $localize(_templateObject(), 1, 2)`); - const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall); + const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall, fs); expect(parts).toEqual(['a', 'b\t', 'c']); expect(parts.raw).toEqual(['a', 'b\\t', 'c']); expect(locations).toEqual([ @@ -173,7 +175,7 @@ runInEachFileSystem(() => { const localizeStatement = localizeCall.parentPath as NodePath; const statements = localizeStatement.container as object[]; expect(statements.length).toEqual(2); - unwrapMessagePartsFromLocalizeCall(localizeCall); + unwrapMessagePartsFromLocalizeCall(localizeCall, fs); expect(statements.length).toEqual(1); expect(statements[0]).toBe(localizeStatement.node); }); @@ -183,7 +185,7 @@ runInEachFileSystem(() => { it('should return the substitutions and locations from a direct call to a tag function', () => { const call = getLocalizeCall(`$localize(['a', 'b\t', 'c'], 1, 2)`); - const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call); + const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call, fs); expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]); expect(locations).toEqual([ { @@ -204,7 +206,7 @@ runInEachFileSystem(() => { it('should return the substitutions and locations from a downleveled tagged template', () => { const call = getLocalizeCall( `$localize(__makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), 1, 2)`); - const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call); + const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call, fs); expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]); expect(locations).toEqual([ { @@ -226,7 +228,8 @@ runInEachFileSystem(() => { describe('unwrapMessagePartsFromTemplateLiteral', () => { it('should return a TemplateStringsArray built from the template literal elements', () => { const taggedTemplate = getTaggedTemplate('$localize `a${1}b\\t${2}c`;'); - expect(unwrapMessagePartsFromTemplateLiteral(taggedTemplate.get('quasi').get('quasis'))[0]) + expect( + unwrapMessagePartsFromTemplateLiteral(taggedTemplate.get('quasi').get('quasis'), fs)[0]) .toEqual(ɵmakeTemplateObject(['a', 'b\t', 'c'], ['a', 'b\\t', 'c'])); }); }); @@ -248,7 +251,7 @@ runInEachFileSystem(() => { describe('unwrapStringLiteralArray', () => { it('should return an array of string from an array expression', () => { const array = getFirstExpression(`['a', 'b', 'c']`); - const [expressions, locations] = unwrapStringLiteralArray(array); + const [expressions, locations] = unwrapStringLiteralArray(array, fs); expect(expressions).toEqual(['a', 'b', 'c']); expect(locations).toEqual([ { @@ -274,7 +277,7 @@ runInEachFileSystem(() => { it('should throw an error if any elements of the array are not literal strings', () => { const array = getFirstExpression(`['a', 2, 'c']`); - expect(() => unwrapStringLiteralArray(array)) + expect(() => unwrapStringLiteralArray(array, fs)) .toThrowError( 'Unexpected messageParts for `$localize` (expected an array of strings).'); }); @@ -315,7 +318,7 @@ runInEachFileSystem(() => { filename: 'src/test.js', sourceRoot: '/root', }); - const location = getLocation(taggedTemplate)!; + const location = getLocation(fs, taggedTemplate)!; expect(location).toBeDefined(); expect(location.start).toEqual({line: 0, column: 10}); expect(location.start.constructor.name).toEqual('Object'); @@ -327,7 +330,7 @@ runInEachFileSystem(() => { it('should return `undefined` if the NodePath has no filename', () => { const taggedTemplate = getTaggedTemplate( 'const x = $localize ``;', {sourceRoot: '/root', filename: undefined}); - const location = getLocation(taggedTemplate); + const location = getLocation(fs, taggedTemplate); expect(location).toBeUndefined(); }); }); diff --git a/packages/localize/src/tools/test/translate/source_files/es2015_translate_plugin_spec.ts b/packages/localize/src/tools/test/translate/source_files/es2015_translate_plugin_spec.ts index 5206ee47f24dd..23a5ed343b72a 100644 --- a/packages/localize/src/tools/test/translate/source_files/es2015_translate_plugin_spec.ts +++ b/packages/localize/src/tools/test/translate/source_files/es2015_translate_plugin_spec.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {ɵcomputeMsgId, ɵparseTranslation} from '@angular/localize'; import {ɵParsedTranslation} from '@angular/localize/private'; @@ -15,6 +16,8 @@ import {TranslatePluginOptions} from '../../../src/source_file_utils'; import {makeEs2015TranslatePlugin} from '../../../src/translate/source_files/es2015_translate_plugin'; runInEachFileSystem(() => { + let fs: FileSystem; + beforeEach(() => fs = getFileSystem()); describe('makeEs2015Plugin', () => { describe('(no translations)', () => { it('should transform `$localize` tags with binary expression', () => { @@ -168,13 +171,13 @@ runInEachFileSystem(() => { }); }); }); -}); -function transformCode( - input: string, translations: Record = {}, - pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string { - return transformSync(input, { - plugins: [makeEs2015TranslatePlugin(diagnostics, translations, pluginOptions)], - filename: '/app/dist/test.js' - })!.code!; -} + function transformCode( + input: string, translations: Record = {}, + pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string { + return transformSync(input, { + plugins: [makeEs2015TranslatePlugin(diagnostics, translations, pluginOptions)], + filename: '/app/dist/test.js' + })!.code!; + } +}); diff --git a/packages/localize/src/tools/test/translate/source_files/es5_translate_plugin_spec.ts b/packages/localize/src/tools/test/translate/source_files/es5_translate_plugin_spec.ts index 99c815d11afbf..088113f405e4f 100644 --- a/packages/localize/src/tools/test/translate/source_files/es5_translate_plugin_spec.ts +++ b/packages/localize/src/tools/test/translate/source_files/es5_translate_plugin_spec.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {ɵcomputeMsgId, ɵparseTranslation} from '@angular/localize'; import {ɵParsedTranslation} from '@angular/localize/private'; @@ -15,6 +16,8 @@ import {TranslatePluginOptions} from '../../../src/source_file_utils'; import {makeEs5TranslatePlugin} from '../../../src/translate/source_files/es5_translate_plugin'; runInEachFileSystem(() => { + let fs: FileSystem; + beforeEach(() => fs = getFileSystem()); describe('makeEs5Plugin', () => { describe('(no translations)', () => { it('should transform `$localize` calls with binary expression', () => { @@ -357,13 +360,13 @@ runInEachFileSystem(() => { expect(output).toEqual('"abc" + (1 + 2 + 3) + " - Hello, " + getName() + "!";'); }); }); -}); -function transformCode( - input: string, translations: Record = {}, - pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string { - return transformSync(input, { - plugins: [makeEs5TranslatePlugin(diagnostics, translations, pluginOptions)], - filename: '/app/dist/test.js' - })!.code!; -} + function transformCode( + input: string, translations: Record = {}, + pluginOptions?: TranslatePluginOptions, diagnostics = new Diagnostics()): string { + return transformSync(input, { + plugins: [makeEs5TranslatePlugin(diagnostics, translations, pluginOptions)], + filename: '/app/dist/test.js' + })!.code!; + } +});