packages/babel-plugin-fbt/src/fbt-nodes/FbtArguments.js (146 lines of code) (raw):

/** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * @emails oncall+i18n_fbt_js * @flow * @format */ /*eslint max-len: ["error", 100]*/ 'use strict'; import type {EnumKey} from '../FbtEnumRegistrar'; import type {GenderConstEnum} from '../Gender'; import typeof { EXACTLY_ONE, GENDER_ANY, NUMBER_ANY, } from '../translate/IntlVariations'; import type FbtNode, {AnyFbtNode} from './FbtNode'; const {compactBabelNodeProps, getRawSource, varDump} = require('../FbtUtil'); const invariant = require('invariant'); export type AnyStringVariationArg = | EnumStringVariationArg | GenderStringVariationArg | NumberStringVariationArg; export type AnyFbtArgument = GenericArg | AnyStringVariationArg; /** * Base class representing fbt construct arguments that support dynamic values at runtime. * * E.g. * * <fbt:plural * count={ * numParticipants <-- FbtArgumentBase * } * value={ * formatted(numParticipants) <-- FbtArgumentBase * } * showCount="yes" <-- hard-coded, so not an FbtArgumentBase * > * challenger * </fbt:plural> */ class FbtArgumentBase<B: ?BabelNode> { // Reference of the FbtNode creator of this instance +fbtNode: AnyFbtNode; // BabelNode representing the value of this argument +node: B; constructor(fbtNode: AnyFbtNode, node: B) { this.fbtNode = fbtNode; this.node = node; } /** * For debugging and unit tests: * * Since BabelNode objects are pretty deep and filled with low-level properties * that we don't really care about, we'll process any BabelNode property of this object so that: * * - we convert the property value to a string like `'BabelNode[type=SomeBabelType]'` * - we add a new property like `__*propName*Code` whose value will * be the JS source code of the original BabelNode. * * See snapshot `fbtFunctional-test.js.snap` to find output examples. */ __toJSONForTestsOnly(): mixed { const {fbtNode} = this; const ret = compactBabelNodeProps({ ...this, fbtNode: fbtNode != null ? fbtNode.constructor.name : fbtNode, }); Object.defineProperty(ret, 'constructor', { value: this.constructor, enumerable: false, }); return ret; } toJSON(): mixed { return this.__toJSONForTestsOnly(); } getArgCode(code: string): string { invariant( !!this.node, 'Unable to find Babel node object from string variation argument: %s', varDump(this), ); return getRawSource(code, this.node); } } /** * Special fbt argument that does NOT produce string variations. * * E.g. * * <fbt:plural * count={ * numParticipants <-- NumberStringVariationArg * } * value={ * formatted(numParticipants) <-- GenericArg (used for UI display only) * } * showCount="yes" * > * challenger * </fbt:plural> */ class GenericArg extends FbtArgumentBase<BabelNode> {} /** * Given an fbt callsite that may generate multiple string variations, * we know that these variations are issued from some specific arguments. * * This is the base class that represents these string variation arguments. * * I.e. * * fbt( * [ * 'Wish ', * fbt.pronoun( * 'object', * personGender, // <-- the string varation argument * {human: true} * ), * ' a happy birthday.', * ], * 'text with pronoun', * ); * * The string variation argument would be based on the `personGender` variable. */ class StringVariationArg< Value, B: ?BabelNode = BabelNode, > extends FbtArgumentBase<B> { /** * List of candidate values that this SVArgument might have. */ +candidateValues: $ReadOnlyArray<Value>; /** * Current SVArgument value of this instance among candidates from `candidateValues`. */ +value: ?Value; /** * Given a list of SV arguments, some of them can be omitted because they're "redundant". * Note: a SV argument cam be omitted because another one of the same type and same BabelNode * source code expression already exist in the list of SV arguments. * Set this property to `true` if that's the case. */ +isCollapsible: boolean; constructor( fbtNode: AnyFbtNode, node: B, candidateValues: $ReadOnlyArray<Value>, value: ?Value, isCollapsible: boolean = false, ) { super(fbtNode, node); this.candidateValues = candidateValues; this.value = value; this.isCollapsible = isCollapsible; } cloneWithValue(value: Value, isCollapsible: boolean): this { return new this.constructor( this.fbtNode, this.node, this.candidateValues, value, isCollapsible, ); } } /** * String variation argument that produces variations based on a string enum */ class EnumStringVariationArg extends StringVariationArg<EnumKey> { static assert(value: mixed): EnumStringVariationArg { return assertInstanceOf(value, EnumStringVariationArg); } } /** * String variation argument that produces variations based on genders */ class GenderStringVariationArg extends StringVariationArg< GenderConstEnum | GENDER_ANY, > { static assert(value: mixed): GenderStringVariationArg { return assertInstanceOf(value, GenderStringVariationArg); } } /** * String variation argument that produces variations based on numbers */ class NumberStringVariationArg extends StringVariationArg< NUMBER_ANY | EXACTLY_ONE, ?BabelNode, > { static assert(value: mixed): NumberStringVariationArg { return assertInstanceOf(value, NumberStringVariationArg); } } function assertInstanceOf<C: interface {}>( value: mixed, Constructor: Class<C> & {name: string}, ): C { invariant( value instanceof Constructor, 'Expected instance of %s but got instead: (%s) %s', Constructor.name, typeof value, varDump(value), ); return value; } /** * Map of string variation arguments keyed by their source FbtNode */ class StringVariationArgsMap { +_map: Map<AnyFbtNode, AnyStringVariationArg>; constructor(svArgs: $ReadOnlyArray<AnyStringVariationArg>): void { this._map = new Map(svArgs.map(arg => [arg.fbtNode, arg])); invariant( svArgs.length === this._map.size, 'Expected only one StringVariationArg per FbtNode. ' + 'Input array length=%s but resulting map size=%s', svArgs.length, this._map.size, ); } /** * @return StringVariationArg corresponding to the given FbtNode */ get<SV: AnyStringVariationArg>(fbtNode: FbtNode<SV, any, any, any>): SV { const ret = this._map.get(fbtNode); invariant( ret != null, 'Unable to find entry for FbtNode: %s', varDump(fbtNode), ); // $FlowFixMe[incompatible-return] the found SVArgument came from the same fbtNode return ret; } /** * @throws if the given FbtNode cannot be found */ mustHave<SV: AnyStringVariationArg>( fbtNode: FbtNode<SV, any, any, any>, ): void { this.get(fbtNode); } } module.exports = { EnumStringVariationArg, FbtArgumentBase, GenderStringVariationArg, GenericArg, NumberStringVariationArg, StringVariationArg, StringVariationArgsMap, };