@samitouri / QOS-React / commits / d5e2d9f8d5

Handle fbt:param with only leading or trailing whitespace

Fixes T180504437. We expected `<fbt:param>` to always have no surrounding whitespace or have both leading and trailing whitespace, it can have one but not the other, though such cases are rare in practice.

Joe Savona committed Mar 13, 2024 at 14:54 UTC d5e2d9f8d594f81693b24d0472b5109388cdbd0c
7 files changed +272 -57
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+8 -16
@@ -1495,23 +1495,15 @@ function codegenInstructionValue(
1495 tagValue.type === "StringLiteral" &&
1496 SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
1497 ) {
1498 - CompilerError.invariant(
1499 - instrValue.children != null &&
1500 - (instrValue.children.length === 3 ||
1501 - instrValue.children.length === 1),
1502 - {
1503 - loc: instrValue.loc,
1504 - reason:
1505 - "Expected fbt element to have 3 children (whitespace, content, whitespace) or 1 (content)",
1506 - suggestions: null,
1507 - description: null,
1508 - }
1498 + CompilerError.invariant(instrValue.children != null, {
1499 + loc: instrValue.loc,
1500 + reason: "Expected fbt element to have children",
1501 + suggestions: null,
1502 + description: null,
1503 + });
1504 + children = instrValue.children.map((child) =>
1505 + codegenJsxFbtChildElement(cx, child)
1506 );
1510 - if (instrValue.children.length === 3) {
1511 - children = [codegenJsxFbtChildElement(cx, instrValue.children[1]!)];
1512 - } else {
1513 - children = [codegenJsxFbtChildElement(cx, instrValue.children[0]!)];
1514 - }
1507 } else {
1508 children =
1509 instrValue.children !== null
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-fbt-with-additional-whitespace-child.expect.md deleted
-31
@@ -1,31 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component(props) {
6 - return (
7 - <fbt desc="Title">
8 - <fbt:plural count={identity(props.count)} name="count" showCount="yes">
9 - vote
10 - </fbt:plural>{" "}
11 - for <fbt:param name="option">{props.option} </fbt:param>
12 - </fbt>
13 - );
14 -}
15 -
16 -```
17 -
18 -
19 -## Error
20 -
21 -```
22 - 5 | vote
23 - 6 | </fbt:plural>{" "}
24 -> 7 | for <fbt:param name="option">{props.option} </fbt:param>
25 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [ReactForget] Invariant: Expected fbt element to have 3 children (whitespace, content, whitespace) or 1 (content) (7:7)
26 - 8 | </fbt>
27 - 9 | );
28 - 10 | }
29 -```
30 -
31 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-fbt-with-additional-whitespace-child.js deleted
-10
@@ -1,10 +0,0 @@
1 -function Component(props) {
2 - return (
3 - <fbt desc="Title">
4 - <fbt:plural count={identity(props.count)} name="count" showCount="yes">
5 - vote
6 - </fbt:plural>{" "}
7 - for <fbt:param name="option">{props.option} </fbt:param>
8 - </fbt>
9 - );
10 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbt/fbt-param-with-leading-whitespace.expect.md new
+101
@@ -0,0 +1,101 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import fbt from "fbt";
6 +import { identity } from "shared-runtime";
7 +
8 +function Component(props) {
9 + return (
10 + <span>
11 + <fbt desc="Title">
12 + <fbt:plural count={identity(props.count)} name="count" showCount="yes">
13 + vote
14 + </fbt:plural>{" "}
15 + for <fbt:param name="option"> {props.option}</fbt:param>
16 + </fbt>
17 + !
18 + </span>
19 + );
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{ count: 42, option: "thing" }],
25 + sequentialRenders: [
26 + { count: 42, option: "thing" },
27 + { count: 42, option: "thing" },
28 + { count: 1, option: "other" },
29 + { count: 1, option: "other" },
30 + { count: 42, option: "thing" },
31 + { count: 1, option: "other" },
32 + { count: 42, option: "thing" },
33 + { count: 1, option: "other" },
34 + ],
35 +};
36 +
37 +```
38 +
39 +## Code
40 +
41 +```javascript
42 +import { unstable_useMemoCache as useMemoCache } from "react";
43 +import fbt from "fbt";
44 +import { identity } from "shared-runtime";
45 +
46 +function Component(props) {
47 + const $ = useMemoCache(3);
48 + let t0;
49 + if ($[0] !== props.count || $[1] !== props.option) {
50 + t0 = (
51 + <span>
52 + {fbt._(
53 + { "*": "{count} votes for {option}", _1: "1 vote for {option}" },
54 + [
55 + fbt._plural(identity(props.count), "count"),
56 + fbt._param(
57 + "option",
58 +
59 + props.option
60 + ),
61 + ],
62 + { hk: "3Bg20a" }
63 + )}
64 + !
65 + </span>
66 + );
67 + $[0] = props.count;
68 + $[1] = props.option;
69 + $[2] = t0;
70 + } else {
71 + t0 = $[2];
72 + }
73 + return t0;
74 +}
75 +
76 +export const FIXTURE_ENTRYPOINT = {
77 + fn: Component,
78 + params: [{ count: 42, option: "thing" }],
79 + sequentialRenders: [
80 + { count: 42, option: "thing" },
81 + { count: 42, option: "thing" },
82 + { count: 1, option: "other" },
83 + { count: 1, option: "other" },
84 + { count: 42, option: "thing" },
85 + { count: 1, option: "other" },
86 + { count: 42, option: "thing" },
87 + { count: 1, option: "other" },
88 + ],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) <span>42 votes for thing!</span>
95 +<span>42 votes for thing!</span>
96 +<span>1 vote for other!</span>
97 +<span>1 vote for other!</span>
98 +<span>42 votes for thing!</span>
99 +<span>1 vote for other!</span>
100 +<span>42 votes for thing!</span>
101 +<span>1 vote for other!</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbt/fbt-param-with-leading-whitespace.js new
+31
@@ -0,0 +1,31 @@
1 +import fbt from "fbt";
2 +import { identity } from "shared-runtime";
3 +
4 +function Component(props) {
5 + return (
6 + <span>
7 + <fbt desc="Title">
8 + <fbt:plural count={identity(props.count)} name="count" showCount="yes">
9 + vote
10 + </fbt:plural>{" "}
11 + for <fbt:param name="option"> {props.option}</fbt:param>
12 + </fbt>
13 + !
14 + </span>
15 + );
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ count: 42, option: "thing" }],
21 + sequentialRenders: [
22 + { count: 42, option: "thing" },
23 + { count: 42, option: "thing" },
24 + { count: 1, option: "other" },
25 + { count: 1, option: "other" },
26 + { count: 42, option: "thing" },
27 + { count: 1, option: "other" },
28 + { count: 42, option: "thing" },
29 + { count: 1, option: "other" },
30 + ],
31 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbt/fbt-param-with-trailing-whitespace.expect.md new
+101
@@ -0,0 +1,101 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import fbt from "fbt";
6 +import { identity } from "shared-runtime";
7 +
8 +function Component(props) {
9 + return (
10 + <span>
11 + <fbt desc="Title">
12 + <fbt:plural count={identity(props.count)} name="count" showCount="yes">
13 + vote
14 + </fbt:plural>{" "}
15 + for <fbt:param name="option">{props.option} </fbt:param>
16 + </fbt>
17 + !
18 + </span>
19 + );
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{ count: 42, option: "thing" }],
25 + sequentialRenders: [
26 + { count: 42, option: "thing" },
27 + { count: 42, option: "thing" },
28 + { count: 1, option: "other" },
29 + { count: 1, option: "other" },
30 + { count: 42, option: "thing" },
31 + { count: 1, option: "other" },
32 + { count: 42, option: "thing" },
33 + { count: 1, option: "other" },
34 + ],
35 +};
36 +
37 +```
38 +
39 +## Code
40 +
41 +```javascript
42 +import { unstable_useMemoCache as useMemoCache } from "react";
43 +import fbt from "fbt";
44 +import { identity } from "shared-runtime";
45 +
46 +function Component(props) {
47 + const $ = useMemoCache(3);
48 + let t0;
49 + if ($[0] !== props.count || $[1] !== props.option) {
50 + t0 = (
51 + <span>
52 + {fbt._(
53 + { "*": "{count} votes for {option}", _1: "1 vote for {option}" },
54 + [
55 + fbt._plural(identity(props.count), "count"),
56 + fbt._param(
57 + "option",
58 +
59 + props.option
60 + ),
61 + ],
62 + { hk: "3Bg20a" }
63 + )}
64 + !
65 + </span>
66 + );
67 + $[0] = props.count;
68 + $[1] = props.option;
69 + $[2] = t0;
70 + } else {
71 + t0 = $[2];
72 + }
73 + return t0;
74 +}
75 +
76 +export const FIXTURE_ENTRYPOINT = {
77 + fn: Component,
78 + params: [{ count: 42, option: "thing" }],
79 + sequentialRenders: [
80 + { count: 42, option: "thing" },
81 + { count: 42, option: "thing" },
82 + { count: 1, option: "other" },
83 + { count: 1, option: "other" },
84 + { count: 42, option: "thing" },
85 + { count: 1, option: "other" },
86 + { count: 42, option: "thing" },
87 + { count: 1, option: "other" },
88 + ],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) <span>42 votes for thing!</span>
95 +<span>42 votes for thing!</span>
96 +<span>1 vote for other!</span>
97 +<span>1 vote for other!</span>
98 +<span>42 votes for thing!</span>
99 +<span>1 vote for other!</span>
100 +<span>42 votes for thing!</span>
101 +<span>1 vote for other!</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbt/fbt-param-with-trailing-whitespace.js new
+31
@@ -0,0 +1,31 @@
1 +import fbt from "fbt";
2 +import { identity } from "shared-runtime";
3 +
4 +function Component(props) {
5 + return (
6 + <span>
7 + <fbt desc="Title">
8 + <fbt:plural count={identity(props.count)} name="count" showCount="yes">
9 + vote
10 + </fbt:plural>{" "}
11 + for <fbt:param name="option">{props.option} </fbt:param>
12 + </fbt>
13 + !
14 + </span>
15 + );
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ count: 42, option: "thing" }],
21 + sequentialRenders: [
22 + { count: 42, option: "thing" },
23 + { count: 42, option: "thing" },
24 + { count: 1, option: "other" },
25 + { count: 1, option: "other" },
26 + { count: 42, option: "thing" },
27 + { count: 1, option: "other" },
28 + { count: 42, option: "thing" },
29 + { count: 1, option: "other" },
30 + ],
31 +};