Skip to content

Commit ca9779e

Browse files
committed
[compiler] Provide support for custom fbt-like macro functions
ghstack-source-id: 6c05e7aa20ba8cbd19c932b052ce4c744ed5f07b Pull Request resolved: #29893
1 parent 2ba462b commit ca9779e

File tree

7 files changed

+199
-12
lines changed

7 files changed

+199
-12
lines changed

Diff for: compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

+12
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ export type Hook = z.infer<typeof HookSchema>;
119119
const EnvironmentConfigSchema = z.object({
120120
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
121121

122+
/**
123+
* A list of functions which the application compiles as macros, where
124+
* the compiler must ensure they are not compiled to rename the macro or separate the
125+
* "function" from its argument.
126+
*
127+
* For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which
128+
* are rewritten by a plugin. Assigning `featureflag` to a temporary would break the
129+
* plugin since it looks specifically for the name of the function being invoked, not
130+
* following aliases.
131+
*/
132+
customMacros: z.nullable(z.array(z.string())).default(null),
133+
122134
/**
123135
* Enable a check that resets the memoization cache when the source code of the file changes.
124136
* This is intended to support hot module reloading (HMR), where the same runtime component

Diff for: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { Err, Ok, Result } from "../Utils/Result";
4343
import { GuardKind } from "../Utils/RuntimeDiagnosticConstants";
4444
import { assertExhaustive } from "../Utils/utils";
4545
import { buildReactiveFunction } from "./BuildReactiveFunction";
46-
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtOperandsInSameScope";
46+
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtAndMacroOperandsInSameScope";
4747
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
4848

4949
export const MEMO_CACHE_SENTINEL = "react.memo_cache_sentinel";

Diff for: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts renamed to compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import prettyFormat from "pretty-format";
89
import {
910
HIRFunction,
1011
IdentifierId,
@@ -14,8 +15,15 @@ import {
1415
} from "../HIR";
1516
import { eachReactiveValueOperand } from "./visitors";
1617

17-
/*
18-
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/).
18+
/**
19+
* This pass supports the
20+
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
21+
* as well as similar user-configurable macro-like APIs where it's important that
22+
* the name of the function not be changed, and it's literal arguments not be turned
23+
* into temporaries.
24+
*
25+
* ## FBT
26+
*
1927
* FBT provides the `<fbt>` JSX element and `fbt()` calls (which take params in the
2028
* form of `<fbt:param>` children or `fbt.param()` arguments, respectively). These
2129
* tags/functions have restrictions on what types of syntax may appear as props/children/
@@ -26,13 +34,22 @@ import { eachReactiveValueOperand } from "./visitors";
2634
* operands to fbt tags/calls have the same scope as the tag/call itself.
2735
*
2836
* Note that this still allows the props/arguments of `<fbt:param>`/`fbt.param()`
29-
* to be independently memoized
37+
* to be independently memoized.
38+
*
39+
* ## User-defined macro-like function
40+
*
41+
* Users can also specify their own functions to be treated similarly to fbt via the
42+
* `customMacros` environment configuration.
3043
*/
31-
export function memoizeFbtOperandsInSameScope(fn: HIRFunction): void {
44+
export function memoizeFbtAndMacroOperandsInSameScope(fn: HIRFunction): void {
45+
const fbtMacroTags = new Set([
46+
...FBT_TAGS,
47+
...(fn.env.config.customMacros ?? []),
48+
]);
3249
const fbtValues: Set<IdentifierId> = new Set();
3350
while (true) {
3451
let size = fbtValues.size;
35-
visit(fn, fbtValues);
52+
visit(fn, fbtMacroTags, fbtValues);
3653
if (size === fbtValues.size) {
3754
break;
3855
}
@@ -50,7 +67,11 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
5067
"fbs:param",
5168
]);
5269

53-
function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
70+
function visit(
71+
fn: HIRFunction,
72+
fbtMacroTags: Set<string>,
73+
fbtValues: Set<IdentifierId>
74+
): void {
5475
for (const [, block] of fn.body.blocks) {
5576
for (const instruction of block.instructions) {
5677
const { lvalue, value } = instruction;
@@ -60,7 +81,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
6081
if (
6182
value.kind === "Primitive" &&
6283
typeof value.value === "string" &&
63-
FBT_TAGS.has(value.value)
84+
fbtMacroTags.has(value.value)
6485
) {
6586
/*
6687
* We don't distinguish between tag names and strings, so record
@@ -69,7 +90,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
6990
fbtValues.add(lvalue.identifier.id);
7091
} else if (
7192
value.kind === "LoadGlobal" &&
72-
FBT_TAGS.has(value.binding.name)
93+
fbtMacroTags.has(value.binding.name)
7394
) {
7495
// Record references to `fbt` as a global
7596
fbtValues.add(lvalue.identifier.id);
@@ -96,7 +117,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
96117
);
97118
}
98119
} else if (
99-
isFbtJsxExpression(fbtValues, value) ||
120+
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
100121
isFbtJsxChild(fbtValues, lvalue, value)
101122
) {
102123
const fbtScope = lvalue.identifier.scope;
@@ -141,14 +162,15 @@ function isFbtCallExpression(
141162
}
142163

143164
function isFbtJsxExpression(
165+
fbtMacroTags: Set<string>,
144166
fbtValues: Set<IdentifierId>,
145167
value: ReactiveValue
146168
): boolean {
147169
return (
148170
value.kind === "JsxExpression" &&
149171
((value.tag.kind === "Identifier" &&
150172
fbtValues.has(value.tag.identifier.id)) ||
151-
(value.tag.kind === "BuiltinTag" && FBT_TAGS.has(value.tag.name)))
173+
(value.tag.kind === "BuiltinTag" && fbtMacroTags.has(value.tag.name)))
152174
);
153175
}
154176

Diff for: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclara
1919
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
2020
export { flattenScopesWithHooksOrUse } from "./FlattenScopesWithHooksOrUse";
2121
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
22-
export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
22+
export { memoizeFbtAndMacroOperandsInSameScope as memoizeFbtOperandsInSameScope } from "./MemoizeFbtAndMacroOperandsInSameScope";
2323
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
2424
export { mergeReactiveScopesThatInvalidateTogether } from "./MergeReactiveScopesThatInvalidateTogether";
2525
export { printReactiveFunction } from "./PrintReactiveFunction";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
6+
import { identity } from "shared-runtime";
7+
8+
const DARK = "dark";
9+
10+
function Component() {
11+
const theme = useTheme();
12+
return (
13+
<div
14+
className={cx({
15+
"styles/light": true,
16+
"styles/dark": theme.getTheme() === DARK,
17+
})}
18+
/>
19+
);
20+
}
21+
22+
function cx(obj) {
23+
const classes = [];
24+
for (const [key, value] of Object.entries(obj)) {
25+
if (value) {
26+
classes.push(key);
27+
}
28+
}
29+
return classes.join(" ");
30+
}
31+
32+
function useTheme() {
33+
return {
34+
getTheme() {
35+
return DARK;
36+
},
37+
};
38+
}
39+
40+
export const FIXTURE_ENTRYPOINT = {
41+
fn: Component,
42+
params: [{}],
43+
};
44+
45+
```
46+
47+
## Code
48+
49+
```javascript
50+
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
51+
import { identity } from "shared-runtime";
52+
53+
const DARK = "dark";
54+
55+
function Component() {
56+
const $ = _c(2);
57+
const theme = useTheme();
58+
59+
const t0 = cx({
60+
"styles/light": true,
61+
"styles/dark": theme.getTheme() === DARK,
62+
});
63+
let t1;
64+
if ($[0] !== t0) {
65+
t1 = <div className={t0} />;
66+
$[0] = t0;
67+
$[1] = t1;
68+
} else {
69+
t1 = $[1];
70+
}
71+
return t1;
72+
}
73+
74+
function cx(obj) {
75+
const classes = [];
76+
for (const [key, value] of Object.entries(obj)) {
77+
if (value) {
78+
classes.push(key);
79+
}
80+
}
81+
return classes.join(" ");
82+
}
83+
84+
function useTheme() {
85+
return {
86+
getTheme() {
87+
return DARK;
88+
},
89+
};
90+
}
91+
92+
export const FIXTURE_ENTRYPOINT = {
93+
fn: Component,
94+
params: [{}],
95+
};
96+
97+
```
98+
99+
### Eval output
100+
(kind: ok) <div class="styles/light styles/dark"></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
2+
import { identity } from "shared-runtime";
3+
4+
const DARK = "dark";
5+
6+
function Component() {
7+
const theme = useTheme();
8+
return (
9+
<div
10+
className={cx({
11+
"styles/light": true,
12+
"styles/dark": theme.getTheme() === DARK,
13+
})}
14+
/>
15+
);
16+
}
17+
18+
function cx(obj) {
19+
const classes = [];
20+
for (const [key, value] of Object.entries(obj)) {
21+
if (value) {
22+
classes.push(key);
23+
}
24+
}
25+
return classes.join(" ");
26+
}
27+
28+
function useTheme() {
29+
return {
30+
getTheme() {
31+
return DARK;
32+
},
33+
};
34+
}
35+
36+
export const FIXTURE_ENTRYPOINT = {
37+
fn: Component,
38+
params: [{}],
39+
};

Diff for: compiler/packages/snap/src/compiler.ts

+14
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function makePluginOptions(
4646
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
4747
let validatePreserveExistingMemoizationGuarantees = false;
4848
let enableChangeDetectionForDebugging = null;
49+
let customMacros = null;
4950

5051
if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
5152
assert(
@@ -142,6 +143,18 @@ function makePluginOptions(
142143
);
143144
}
144145

146+
const customMacrosMatch = /@customMacros\(([^)]+)\)/.exec(firstLine);
147+
if (
148+
customMacrosMatch &&
149+
customMacrosMatch.length > 1 &&
150+
customMacrosMatch[1].trim().length > 0
151+
) {
152+
customMacros = customMacrosMatch[1]
153+
.split(" ")
154+
.map((s) => s.trim())
155+
.filter((s) => s.length > 0);
156+
}
157+
145158
let logs: Array<{ filename: string | null; event: LoggerEvent }> = [];
146159
let logger: Logger | null = null;
147160
if (firstLine.includes("@logger")) {
@@ -185,6 +198,7 @@ function makePluginOptions(
185198
},
186199
],
187200
]),
201+
customMacros,
188202
enableEmitFreeze,
189203
enableEmitInstrumentForget,
190204
enableEmitHookGuards,

0 commit comments

Comments
 (0)