fbt:param does not allow jsxtext children
Per the title, `<fbt:param>0</fbt:param>` is invalid FBT, you must wrap the text in an expression container. But that's not all, `fbt:param` can only have a single child, which means we have to strip out the text elements that occur from the whitespace in the source.
Joe Savona committed
Sep 15, 2023 at 11:30 UTC
dc4e63e2d53f91239fe85ac657666c95fde01578
7 files changed
+117
-8
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+53
-4
@@ -34,6 +34,7 @@ import { eachPatternOperand } from "../HIR/visitors";
34
import { Err, Ok, Result } from "../Utils/Result";
35
import { assertExhaustive } from "../Utils/utils";
36
import { buildReactiveFunction } from "./BuildReactiveFunction";
37
+import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtOperandsInSameScope";
38
39
export type CodegenFunction = {
40
type: "CodegenFunction";
@@ -990,10 +991,34 @@ function codegenInstructionValue(
991
tag = createJsxIdentifier(instrValue.loc, tagValue.value);
992
}
993
}
993
- const children =
994
- instrValue.children !== null
995
- ? instrValue.children.map((child) => codegenJsxElement(cx, child))
996
- : [];
994
+ let children;
995
+ if (
996
+ tagValue.type === "StringLiteral" &&
997
+ SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
998
+ ) {
999
+ CompilerError.invariant(
1000
+ instrValue.children != null &&
1001
+ (instrValue.children.length === 3 ||
1002
+ instrValue.children.length === 1),
1003
+ {
1004
+ loc: instrValue.loc,
1005
+ reason:
1006
+ "Expected fbt element to have 3 children (whitespace, content, whitespace) or 1 (content)",
1007
+ suggestions: null,
1008
+ description: null,
1009
+ }
1010
+ );
1011
+ if (instrValue.children.length === 3) {
1012
+ children = [codegenJsxFbtChildElement(cx, instrValue.children[1]!)];
1013
+ } else {
1014
+ children = [codegenJsxFbtChildElement(cx, instrValue.children[0]!)];
1015
+ }
1016
+ } else {
1017
+ children =
1018
+ instrValue.children !== null
1019
+ ? instrValue.children.map((child) => codegenJsxElement(cx, child))
1020
+ : [];
1021
+ }
1022
value = createJsxElement(
1023
instrValue.loc,
1024
t.jsxOpeningElement(tag, attributes, instrValue.children === null),
@@ -1330,6 +1355,30 @@ function codegenJsxElement(
1355
}
1356
}
1357
1358
+function codegenJsxFbtChildElement(
1359
+ cx: Context,
1360
+ place: Place
1361
+):
1362
+ | t.JSXText
1363
+ | t.JSXExpressionContainer
1364
+ | t.JSXSpreadChild
1365
+ | t.JSXElement
1366
+ | t.JSXFragment {
1367
+ const value = codegenPlace(cx, place);
1368
+ switch (value.type) {
1369
+ case "StringLiteral": {
1370
+ return createJsxExpressionContainer(place.loc, value);
1371
+ }
1372
+ case "JSXElement":
1373
+ case "JSXFragment": {
1374
+ return value;
1375
+ }
1376
+ default: {
1377
+ return createJsxExpressionContainer(place.loc, value);
1378
+ }
1379
+ }
1380
+}
1381
+
1382
function convertMemberExpressionToJsx(
1383
expr: t.MemberExpression
1384
): t.JSXMemberExpression {
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts
+2
-1
@@ -44,7 +44,8 @@ export function memoizeFbtOperandsInSameScope(fn: ReactiveFunction): void {
44
}
45
}
46
47
-const FBT_TAGS: Set<string> = new Set(["fbt", "fbt:param"]);
47
+export const FBT_TAGS: Set<string> = new Set(["fbt", "fbt:param"]);
48
+export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set(["fbt:param"]);
49
50
class Transform extends ReactiveFunctionVisitor<void> {
51
// Values that represent *potential* references of `fbt` as a JSX tag name
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-text-must-use-expression-container.expect.md
new
+47
@@ -0,0 +1,47 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import fbt from "fbt";
6
+
7
+function Component(props) {
8
+ return (
9
+ <Foo
10
+ value={
11
+ <fbt desc="Description of the parameter">
12
+ <fbt:param name="value">{"0"}</fbt:param>%
13
+ </fbt>
14
+ }
15
+ />
16
+ );
17
+}
18
+
19
+```
20
+
21
+## Code
22
+
23
+```javascript
24
+import { unstable_useMemoCache as useMemoCache } from "react";
25
+import fbt from "fbt";
26
+
27
+function Component(props) {
28
+ const $ = useMemoCache(2);
29
+ let t0;
30
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
31
+ t0 = fbt._("{value}%", [fbt._param("value", "0")], { hk: "10F5Cc" });
32
+ $[0] = t0;
33
+ } else {
34
+ t0 = $[0];
35
+ }
36
+ let t1;
37
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
38
+ t1 = <Foo value={t0} />;
39
+ $[1] = t1;
40
+ } else {
41
+ t1 = $[1];
42
+ }
43
+ return t1;
44
+}
45
+
46
+```
47
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-text-must-use-expression-container.js
new
+13
@@ -0,0 +1,13 @@
1
+import fbt from "fbt";
2
+
3
+function Component(props) {
4
+ return (
5
+ <Foo
6
+ value={
7
+ <fbt desc="Description of the parameter">
8
+ <fbt:param name="value">{"0"}</fbt:param>%
9
+ </fbt>
10
+ }
11
+ />
12
+ );
13
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-with-jsx-element-content.expect.md
+1
-2
@@ -2,7 +2,6 @@
2
## Input
3
4
```javascript
5
-// @debug
5
import fbt from "fbt";
6
7
function Component({ name, data, icon }) {
@@ -26,7 +25,7 @@ function Component({ name, data, icon }) {
25
## Code
26
27
```javascript
29
-import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
28
+import { unstable_useMemoCache as useMemoCache } from "react";
29
import fbt from "fbt";
30
31
function Component(t39) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-with-jsx-element-content.js
-1
@@ -1,4 +1,3 @@
1
-// @debug
1
import fbt from "fbt";
2
3
function Component({ name, data, icon }) {
compiler/packages/sprout/src/SproutTodoFilter.ts
+1
@@ -455,6 +455,7 @@ const skipFilter = new Set([
455
"infer-skip-components-without-hooks-or-jsx",
456
"class-component-with-render-helper",
457
"fbtparam-with-jsx-element-content",
458
+ "fbtparam-text-must-use-expression-container",
459
]);
460
461
export default skipFilter;