Implement JSX whitespace rules
So far we've been preserving JSX whitespace all the way through to codegen. But JSX has clear rules around whitespace handling, which allows us to trim whitespace in the input in lots of cases. For the most part this doesn't change our output, but I think that’s generally because of prettier. This PR should make a big difference when debugging the compiler, by removing all the whitespace JsxText values. But in some edge-cases it really makes a difference in the output since we can avoid memo slots for strings like `"\n "`.. ## Test Plan * Experimented with our internal tool to verify transform output to confirm that JSXText whitespace does not impact fbt transform results. * Synced and tested profile page, looks fine
Joe Savona committed
Oct 9, 2023 at 16:35 UTC
5a8c7594c5f06908dd2f0afb2cb69897909565a2
8 files changed
+132
-105
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+66
-1
@@ -2829,9 +2829,13 @@ function lowerJsxElement(
2829
return lowerExpressionToTemporary(builder, expression);
2830
}
2831
} else if (exprPath.isJSXText()) {
2832
+ const text = trimJsxText(exprPath.node.value);
2833
+ if (text === null) {
2834
+ return null;
2835
+ }
2836
const place = lowerValueToTemporary(builder, {
2837
kind: "JSXText",
2834
- value: exprPath.node.value,
2838
+ value: text,
2839
loc: exprLoc,
2840
});
2841
return place;
@@ -2851,6 +2855,67 @@ function lowerJsxElement(
2855
}
2856
}
2857
2858
+/**
2859
+ * Trims whitespace according to the JSX spec:
2860
+ * > JSX removes whitespace at the beginning and ending of a line.
2861
+ * > It also removes blank lines. New lines adjacent to tags are removed;
2862
+ * > new lines that occur in the middle of string literals are condensed
2863
+ * > into a single space.
2864
+ *
2865
+ * From https://legacy.reactjs.org/docs/jsx-in-depth.html#string-literals-1
2866
+ *
2867
+ * Implementation adapted from Babel:
2868
+ * https://github.com/babel/babel/blob/54d30f206057be64b496d2da1ec8c49d244ba4e4/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L5
2869
+ */
2870
+function trimJsxText(original: string): string | null {
2871
+ const lines = original.split(/\r\n|\n|\r/);
2872
+
2873
+ let lastNonEmptyLine = 0;
2874
+
2875
+ for (let i = 0; i < lines.length; i++) {
2876
+ if (lines[i].match(/[^ \t]/)) {
2877
+ lastNonEmptyLine = i;
2878
+ }
2879
+ }
2880
+
2881
+ let str = "";
2882
+
2883
+ for (let i = 0; i < lines.length; i++) {
2884
+ const line = lines[i];
2885
+
2886
+ const isFirstLine = i === 0;
2887
+ const isLastLine = i === lines.length - 1;
2888
+ const isLastNonEmptyLine = i === lastNonEmptyLine;
2889
+
2890
+ // replace rendered whitespace tabs with spaces
2891
+ let trimmedLine = line.replace(/\t/g, " ");
2892
+
2893
+ // trim whitespace touching a newline
2894
+ if (!isFirstLine) {
2895
+ trimmedLine = trimmedLine.replace(/^[ ]+/, "");
2896
+ }
2897
+
2898
+ // trim whitespace touching an endline
2899
+ if (!isLastLine) {
2900
+ trimmedLine = trimmedLine.replace(/[ ]+$/, "");
2901
+ }
2902
+
2903
+ if (trimmedLine) {
2904
+ if (!isLastNonEmptyLine) {
2905
+ trimmedLine += " ";
2906
+ }
2907
+
2908
+ str += trimmedLine;
2909
+ }
2910
+ }
2911
+
2912
+ if (str.length !== 0) {
2913
+ return str;
2914
+ } else {
2915
+ return null;
2916
+ }
2917
+}
2918
+
2919
function lowerFunctionExpression(
2920
builder: HIRBuilder,
2921
expr: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/fbtparam-with-jsx-element-content.expect.md
+2
-2
@@ -28,9 +28,9 @@ function Component({ name, data, icon }) {
28
import { unstable_useMemoCache as useMemoCache } from "react";
29
import fbt from "fbt";
30
31
-function Component(t39) {
31
+function Component(t29) {
32
const $ = useMemoCache(4);
33
- const { name, data, icon } = t39;
33
+ const { name, data, icon } = t29;
34
const c_0 = $[0] !== name;
35
const c_1 = $[1] !== icon;
36
const c_2 = $[2] !== data;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/inner-memo-value-not-promoted-to-outer-scope-dynamic.expect.md
+38
-63
@@ -24,101 +24,76 @@ function Component(props) {
24
```javascript
25
import { unstable_useMemoCache as useMemoCache } from "react";
26
function Component(props) {
27
- const $ = useMemoCache(21);
27
+ const $ = useMemoCache(15);
28
const item = useFragment(FRAGMENT, props.item);
29
useFreeze(item);
30
const c_0 = $[0] !== item;
31
let t1;
32
let T2;
33
- let t3;
33
let t0;
35
- let t4;
36
- let T5;
37
- let t6;
34
+ let T3;
35
if (c_0) {
36
const count = new MaybeMutable(item);
37
41
- T5 = View;
42
- t6 = "\n ";
38
+ T3 = View;
39
T2 = View;
44
- t3 = "\n ";
45
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
40
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
41
t0 = <span>Text</span>;
47
- $[8] = t0;
42
+ $[5] = t0;
43
} else {
49
- t0 = $[8];
44
+ t0 = $[5];
45
}
51
- t4 = "\n ";
46
t1 = maybeMutate(count);
47
$[0] = item;
48
$[1] = t1;
49
$[2] = T2;
56
- $[3] = t3;
57
- $[4] = t0;
58
- $[5] = t4;
59
- $[6] = T5;
60
- $[7] = t6;
50
+ $[3] = t0;
51
+ $[4] = T3;
52
} else {
53
t1 = $[1];
54
T2 = $[2];
64
- t3 = $[3];
65
- t0 = $[4];
66
- t4 = $[5];
67
- T5 = $[6];
68
- t6 = $[7];
55
+ t0 = $[3];
56
+ T3 = $[4];
57
}
70
- const c_9 = $[9] !== t1;
71
- let t7;
72
- if (c_9) {
73
- t7 = <span>{t1}</span>;
74
- $[9] = t1;
75
- $[10] = t7;
58
+ const c_6 = $[6] !== t1;
59
+ let t4;
60
+ if (c_6) {
61
+ t4 = <span>{t1}</span>;
62
+ $[6] = t1;
63
+ $[7] = t4;
64
} else {
77
- t7 = $[10];
65
+ t4 = $[7];
66
}
79
- const c_11 = $[11] !== T2;
80
- const c_12 = $[12] !== t3;
81
- const c_13 = $[13] !== t0;
82
- const c_14 = $[14] !== t4;
83
- const c_15 = $[15] !== t7;
84
- let t8;
85
- if (c_11 || c_12 || c_13 || c_14 || c_15) {
86
- t8 = (
67
+ const c_8 = $[8] !== T2;
68
+ const c_9 = $[9] !== t0;
69
+ const c_10 = $[10] !== t4;
70
+ let t5;
71
+ if (c_8 || c_9 || c_10) {
72
+ t5 = (
73
<T2>
88
- {t3}
74
{t0}
75
{t4}
91
- {t7}
76
</T2>
77
);
94
- $[11] = T2;
95
- $[12] = t3;
96
- $[13] = t0;
97
- $[14] = t4;
98
- $[15] = t7;
99
- $[16] = t8;
78
+ $[8] = T2;
79
+ $[9] = t0;
80
+ $[10] = t4;
81
+ $[11] = t5;
82
} else {
101
- t8 = $[16];
83
+ t5 = $[11];
84
}
103
- const c_17 = $[17] !== T5;
104
- const c_18 = $[18] !== t6;
105
- const c_19 = $[19] !== t8;
106
- let t9;
107
- if (c_17 || c_18 || c_19) {
108
- t9 = (
109
- <T5>
110
- {t6}
111
- {t8}
112
- </T5>
113
- );
114
- $[17] = T5;
115
- $[18] = t6;
116
- $[19] = t8;
117
- $[20] = t9;
85
+ const c_12 = $[12] !== T3;
86
+ const c_13 = $[13] !== t5;
87
+ let t6;
88
+ if (c_12 || c_13) {
89
+ t6 = <T3>{t5}</T3>;
90
+ $[12] = T3;
91
+ $[13] = t5;
92
+ $[14] = t6;
93
} else {
119
- t9 = $[20];
94
+ t6 = $[14];
95
}
121
- return t9;
96
+ return t6;
97
}
98
99
```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/inner-memo-value-not-promoted-to-outer-scope-static.expect.md
+18
-33
@@ -21,63 +21,48 @@ function Component(props) {
21
```javascript
22
import { unstable_useMemoCache as useMemoCache } from "react";
23
function Component(props) {
24
- const $ = useMemoCache(9);
24
+ const $ = useMemoCache(6);
25
let t1;
26
let T2;
27
- let t3;
27
let t0;
29
- let t4;
30
- let T5;
31
- let t6;
28
+ let T3;
29
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30
const count = new MaybeMutable();
31
35
- T5 = View;
36
- t6 = "\n ";
32
+ T3 = View;
33
T2 = View;
38
- t3 = "\n ";
39
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
34
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
35
t0 = <span>Text</span>;
41
- $[7] = t0;
36
+ $[4] = t0;
37
} else {
43
- t0 = $[7];
38
+ t0 = $[4];
39
}
45
- t4 = "\n ";
40
t1 = maybeMutate(count);
41
$[0] = t1;
42
$[1] = T2;
49
- $[2] = t3;
50
- $[3] = t0;
51
- $[4] = t4;
52
- $[5] = T5;
53
- $[6] = t6;
43
+ $[2] = t0;
44
+ $[3] = T3;
45
} else {
46
t1 = $[0];
47
T2 = $[1];
57
- t3 = $[2];
58
- t0 = $[3];
59
- t4 = $[4];
60
- T5 = $[5];
61
- t6 = $[6];
48
+ t0 = $[2];
49
+ T3 = $[3];
50
}
63
- let t7;
64
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
65
- t7 = (
66
- <T5>
67
- {t6}
51
+ let t4;
52
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
53
+ t4 = (
54
+ <T3>
55
<T2>
69
- {t3}
56
{t0}
71
- {t4}
57
<span>{t1}</span>
58
</T2>
74
- </T5>
59
+ </T3>
60
);
76
- $[8] = t7;
61
+ $[5] = t4;
62
} else {
78
- t7 = $[8];
63
+ t4 = $[5];
64
}
80
- return t7;
65
+ return t4;
66
}
67
68
```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/jsx-attribute-with-jsx-element-value.expect.md
+2
-2
@@ -43,9 +43,9 @@ export const FIXTURE_ENTRYPOINT = {
43
44
```javascript
45
import { unstable_useMemoCache as useMemoCache } from "react";
46
-function Component(t29) {
46
+function Component(t27) {
47
const $ = useMemoCache(2);
48
- const { items } = t29;
48
+ const { items } = t27;
49
const c_0 = $[0] !== items;
50
let t0;
51
if (c_0) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/merge-consecutive-scopes.expect.md
+1
@@ -78,6 +78,7 @@ function Component() {
78
}
79
return t3;
80
}
81
+
82
export const FIXTURE_ENTRYPOINT = {
83
fn: Component,
84
params: [{ value: 42 }],
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-duplicate-instruction-from-merge-consecutive-scopes.expect.md
+3
-3
@@ -27,9 +27,9 @@ export const FIXTURE_ENTRYPOINT = {
27
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableMergeConsecutiveScopes
28
function Component(id) {
29
const $ = useMemoCache(3);
30
- let t25;
31
- t25 = undefined;
32
- const bar = t25;
30
+ let t22;
31
+ t22 = undefined;
32
+ const bar = t22;
33
let t0;
34
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35
t0 = <Bar title={bar} />;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/timers.expect.md
+2
-1
@@ -35,7 +35,8 @@ function Component(props) {
35
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
36
t1 = (
37
<div>
38
- rendering took {time} at {now}
38
+ rendering took
39
+ {time} at {now}
40
</div>
41
);
42
$[1] = t1;