packages/babel-plugin-fbt-runtime/index.js (127 lines of code) (raw):
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This is a React Native only transform that runs after the fbt syntax
* transform. It extracts jsfbt and strip out extra information from the payload
* produced by the fbt syntax transform.
*
* @emails oncall+i18n_fbt_js
* @flow
* @noformat
*/
'use strict';
/* eslint consistent-return: 0 */
/* eslint max-len: ["warn", 120] */
/*::
import typeof BabelTypes from '@babel/types';
import type {BabelTransformPlugin} from '@babel/core';
import type {SentinelPayload} from 'babel-plugin-fbt/dist/babel-processors/FbtFunctionCallProcessor';
import type {FbtTableKey, PatternString} from '../../runtime/shared/FbtTable';
import type {TableJSFBTTree, TableJSFBTTreeLeaf} from 'babel-plugin-fbt';
import type {FbtRuntimeInput} from 'FbtHooks';
export type PluginOptions = {|
fbtHashKeyModule?: string,
fbtSentinel?: string,
reactNativeMode?: boolean,
|};
*/
const {
FbtUtil: {replaceClearTokensWithTokenAliases},
JSFbtUtil: {mapLeaves},
fbtHashKey: jenkinsHashKey,
} = require('babel-plugin-fbt');
const {shiftEnumsToTop} = require('babel-plugin-fbt').FbtShiftEnums;
const {SENTINEL} = require('babel-plugin-fbt/dist/FbtConstants');
const invariant = require('invariant');
let fbtHashKey /*: typeof jenkinsHashKey */ = jenkinsHashKey;
/**
* Utility function to cast the Babel transform plugin options to the right type
*/
function getPluginOptions(plugin /*: $Shape<{opts: ?PluginOptions}> */) /*: PluginOptions */ {
const {opts} = plugin;
if (opts == null || typeof opts !== 'object') {
// eslint-disable-next-line fb-www/no-new-error
throw new Error(`Expected to opts property to be an object. `
+ `Current value is ${String(opts)} (${typeof opts})`);
}
// $FlowExpectedError[prop-missing]
// $FlowExpectedError[incompatible-exact]
return opts;
}
/**
* Helper method to convert jsfbt tree leaf to runtime input leaf by:
* 1. Stripping away keys (e.g desc and tokenAliases) that are unneccessary
* for runtime and only keep the `text` key.
* 2. Replacing clear token names in the text with mangled tokens.
*/
function convertJSFBTLeafToRuntimeInputText(leaf /*: $ReadOnly<TableJSFBTTreeLeaf> */) /* : PatternString */ {
return replaceClearTokensWithTokenAliases(leaf.text, leaf.tokenAliases);
}
module.exports = function BabelPluginFbtRuntime(babel /*: {
types: BabelTypes,
} */) /*: BabelTransformPlugin */ {
const t = babel.types;
// Need to extract this as a standalone function for Flow type check refinements
const {isCallExpression} = t;
function _buildEnumToHashKeyObjectExpression(
curLevel /*: PatternString | $ReadOnly<TableJSFBTTree> */,
enumsLeft /*: number */,
) /*: BabelNodeObjectExpression */ {
const properties = [];
invariant(typeof curLevel === 'object',
'Expected curLevel to be an object instead of %s', typeof curLevel);
for (const enumKey in curLevel) {
properties.push(
t.objectProperty(
t.identifier(enumKey),
enumsLeft === 1
? t.stringLiteral(fbtHashKey(curLevel[enumKey]))
: _buildEnumToHashKeyObjectExpression(
// TODO(T86653403) Add support for consolidated JSFBT structure to RN
// $FlowFixMe[incompatible-call]
curLevel[enumKey],
enumsLeft - 1,
),
),
);
}
return t.objectExpression(properties);
}
return {
pre() {
// $FlowFixMe[object-this-reference] Babel transforms run with the plugin context by default
const visitor = this;
const opts = getPluginOptions(visitor);
visitor.opts.fbtSentinel = opts.fbtSentinel || SENTINEL;
if (opts.fbtHashKeyModule) {
// $FlowExpectedError[unsupported-syntax] Dynamic import needed
fbtHashKey = require(opts.fbtHashKeyModule);
}
},
name: 'fbt-runtime',
visitor: {
/**
* Transform the following:
* fbt._(
* fbtSentinel +
* JSON.strinfigy({
* type: "text",
* jsfbt: "jsfbt test" | {
* "t": {... jsfbt table}
* ...
* },
* desc: "desc",
* project: "project",
* }) +
* fbtSentinel
* );
* to:
* fbt._("jsfbt test") or fbt._({... jsfbt table})
*/
StringLiteral(path) {
// $FlowFixMe[object-this-reference] Babel transforms run with the plugin context by default
const {fbtSentinel, reactNativeMode} = getPluginOptions(this);
if (fbtSentinel == null || fbtSentinel.trim() == '') {
// eslint-disable-next-line fb-www/no-new-error
throw new Error(`fbtSentinel must be a non-empty string. `
+ `Current value is ${String(fbtSentinel)} (${typeof fbtSentinel})`);
}
const sentinelLength = fbtSentinel.length;
let phrase = path.node.value;
if (
!phrase.startsWith(fbtSentinel) ||
!phrase.endsWith(fbtSentinel) ||
phrase.length <= sentinelLength * 2
) {
return;
}
phrase = (JSON.parse(
phrase.slice(sentinelLength, phrase.length - sentinelLength),
) /*: SentinelPayload */);
const payload = phrase.jsfbt.t;
const runtimeInput = mapLeaves(
payload,
convertJSFBTLeafToRuntimeInputText,
);
// $FlowFixMe[prop-missing] replaceWithSourceString's type is not defined yet
path.replaceWithSourceString(JSON.stringify(runtimeInput));
const parentNode = path.parentPath && path.parentPath.node;
invariant(isCallExpression(parentNode),
'Expected parent node to be a BabelNodeCallExpression');
// Append runtime options - key for runtime dictionary lookup
if (parentNode.arguments.length === 1) {
// Second param 'args' could be omitted sometimes. Use null here
parentNode.arguments.push(t.nullLiteral());
}
invariant(
parentNode.arguments.length === 2,
'Expecting options to be the third param',
);
let shiftedJsfbt;
let enumCount = 0;
if (reactNativeMode) {
({enumCount, shiftedJsfbt} = shiftEnumsToTop(phrase.jsfbt));
}
if (enumCount > 0) {
invariant(
shiftedJsfbt != null,
'Expecting shiftedJsfbt to be defined',
);
parentNode.arguments.push(
// The expected method name is `objectExpression` but
// it already works as-is apparently...
// $FlowFixMe[prop-missing] Use objectExpression() instead
t.ObjectExpression([
t.objectProperty(
t.identifier('ehk'), // enumHashKey
_buildEnumToHashKeyObjectExpression(
shiftedJsfbt,
enumCount,
),
),
]),
);
} else {
parentNode.arguments.push(
// The expected method name is `objectExpression` but
// it already works as-is apparently...
// $FlowFixMe[prop-missing] Use objectExpression() instead
t.ObjectExpression([
t.objectProperty(
t.identifier('hk'),
t.stringLiteral(fbtHashKey(payload)),
),
]),
);
}
},
},
};
};