mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[compiler] Provide support for custom fbt-like macro functions
ghstack-source-id: e3c6455ac2
Pull Request resolved: https://github.com/facebook/react/pull/29893
This commit is contained in:
parent
2ba462b665
commit
a07f5a3db5
|
|
@ -119,6 +119,18 @@ export type Hook = z.infer<typeof HookSchema>;
|
||||||
const EnvironmentConfigSchema = z.object({
|
const EnvironmentConfigSchema = z.object({
|
||||||
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
|
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of functions which the application compiles as macros, where
|
||||||
|
* the compiler must ensure they are not compiled to rename the macro or separate the
|
||||||
|
* "function" from its argument.
|
||||||
|
*
|
||||||
|
* For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which
|
||||||
|
* are rewritten by a plugin. Assigning `featureflag` to a temporary would break the
|
||||||
|
* plugin since it looks specifically for the name of the function being invoked, not
|
||||||
|
* following aliases.
|
||||||
|
*/
|
||||||
|
customMacros: z.nullable(z.array(z.string())).default(null),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable a check that resets the memoization cache when the source code of the file changes.
|
* Enable a check that resets the memoization cache when the source code of the file changes.
|
||||||
* This is intended to support hot module reloading (HMR), where the same runtime component
|
* This is intended to support hot module reloading (HMR), where the same runtime component
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import { Err, Ok, Result } from "../Utils/Result";
|
||||||
import { GuardKind } from "../Utils/RuntimeDiagnosticConstants";
|
import { GuardKind } from "../Utils/RuntimeDiagnosticConstants";
|
||||||
import { assertExhaustive } from "../Utils/utils";
|
import { assertExhaustive } from "../Utils/utils";
|
||||||
import { buildReactiveFunction } from "./BuildReactiveFunction";
|
import { buildReactiveFunction } from "./BuildReactiveFunction";
|
||||||
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtOperandsInSameScope";
|
import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtAndMacroOperandsInSameScope";
|
||||||
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
|
import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
|
||||||
|
|
||||||
export const MEMO_CACHE_SENTINEL = "react.memo_cache_sentinel";
|
export const MEMO_CACHE_SENTINEL = "react.memo_cache_sentinel";
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,15 @@ import {
|
||||||
} from "../HIR";
|
} from "../HIR";
|
||||||
import { eachReactiveValueOperand } from "./visitors";
|
import { eachReactiveValueOperand } from "./visitors";
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/).
|
* This pass supports the
|
||||||
|
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
|
||||||
|
* as well as similar user-configurable macro-like APIs where it's important that
|
||||||
|
* the name of the function not be changed, and it's literal arguments not be turned
|
||||||
|
* into temporaries.
|
||||||
|
*
|
||||||
|
* ## FBT
|
||||||
|
*
|
||||||
* FBT provides the `<fbt>` JSX element and `fbt()` calls (which take params in the
|
* FBT provides the `<fbt>` JSX element and `fbt()` calls (which take params in the
|
||||||
* form of `<fbt:param>` children or `fbt.param()` arguments, respectively). These
|
* form of `<fbt:param>` children or `fbt.param()` arguments, respectively). These
|
||||||
* tags/functions have restrictions on what types of syntax may appear as props/children/
|
* tags/functions have restrictions on what types of syntax may appear as props/children/
|
||||||
|
|
@ -26,13 +33,22 @@ import { eachReactiveValueOperand } from "./visitors";
|
||||||
* operands to fbt tags/calls have the same scope as the tag/call itself.
|
* operands to fbt tags/calls have the same scope as the tag/call itself.
|
||||||
*
|
*
|
||||||
* Note that this still allows the props/arguments of `<fbt:param>`/`fbt.param()`
|
* Note that this still allows the props/arguments of `<fbt:param>`/`fbt.param()`
|
||||||
* to be independently memoized
|
* to be independently memoized.
|
||||||
|
*
|
||||||
|
* ## User-defined macro-like function
|
||||||
|
*
|
||||||
|
* Users can also specify their own functions to be treated similarly to fbt via the
|
||||||
|
* `customMacros` environment configuration.
|
||||||
*/
|
*/
|
||||||
export function memoizeFbtOperandsInSameScope(fn: HIRFunction): void {
|
export function memoizeFbtAndMacroOperandsInSameScope(fn: HIRFunction): void {
|
||||||
|
const fbtMacroTags = new Set([
|
||||||
|
...FBT_TAGS,
|
||||||
|
...(fn.env.config.customMacros ?? []),
|
||||||
|
]);
|
||||||
const fbtValues: Set<IdentifierId> = new Set();
|
const fbtValues: Set<IdentifierId> = new Set();
|
||||||
while (true) {
|
while (true) {
|
||||||
let size = fbtValues.size;
|
let size = fbtValues.size;
|
||||||
visit(fn, fbtValues);
|
visit(fn, fbtMacroTags, fbtValues);
|
||||||
if (size === fbtValues.size) {
|
if (size === fbtValues.size) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +66,11 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||||
"fbs:param",
|
"fbs:param",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
|
function visit(
|
||||||
|
fn: HIRFunction,
|
||||||
|
fbtMacroTags: Set<string>,
|
||||||
|
fbtValues: Set<IdentifierId>
|
||||||
|
): void {
|
||||||
for (const [, block] of fn.body.blocks) {
|
for (const [, block] of fn.body.blocks) {
|
||||||
for (const instruction of block.instructions) {
|
for (const instruction of block.instructions) {
|
||||||
const { lvalue, value } = instruction;
|
const { lvalue, value } = instruction;
|
||||||
|
|
@ -60,7 +80,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
|
||||||
if (
|
if (
|
||||||
value.kind === "Primitive" &&
|
value.kind === "Primitive" &&
|
||||||
typeof value.value === "string" &&
|
typeof value.value === "string" &&
|
||||||
FBT_TAGS.has(value.value)
|
fbtMacroTags.has(value.value)
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
* We don't distinguish between tag names and strings, so record
|
* We don't distinguish between tag names and strings, so record
|
||||||
|
|
@ -69,7 +89,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
|
||||||
fbtValues.add(lvalue.identifier.id);
|
fbtValues.add(lvalue.identifier.id);
|
||||||
} else if (
|
} else if (
|
||||||
value.kind === "LoadGlobal" &&
|
value.kind === "LoadGlobal" &&
|
||||||
FBT_TAGS.has(value.binding.name)
|
fbtMacroTags.has(value.binding.name)
|
||||||
) {
|
) {
|
||||||
// Record references to `fbt` as a global
|
// Record references to `fbt` as a global
|
||||||
fbtValues.add(lvalue.identifier.id);
|
fbtValues.add(lvalue.identifier.id);
|
||||||
|
|
@ -96,7 +116,7 @@ function visit(fn: HIRFunction, fbtValues: Set<IdentifierId>): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
isFbtJsxExpression(fbtValues, value) ||
|
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
|
||||||
isFbtJsxChild(fbtValues, lvalue, value)
|
isFbtJsxChild(fbtValues, lvalue, value)
|
||||||
) {
|
) {
|
||||||
const fbtScope = lvalue.identifier.scope;
|
const fbtScope = lvalue.identifier.scope;
|
||||||
|
|
@ -141,6 +161,7 @@ function isFbtCallExpression(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFbtJsxExpression(
|
function isFbtJsxExpression(
|
||||||
|
fbtMacroTags: Set<string>,
|
||||||
fbtValues: Set<IdentifierId>,
|
fbtValues: Set<IdentifierId>,
|
||||||
value: ReactiveValue
|
value: ReactiveValue
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|
@ -148,7 +169,7 @@ function isFbtJsxExpression(
|
||||||
value.kind === "JsxExpression" &&
|
value.kind === "JsxExpression" &&
|
||||||
((value.tag.kind === "Identifier" &&
|
((value.tag.kind === "Identifier" &&
|
||||||
fbtValues.has(value.tag.identifier.id)) ||
|
fbtValues.has(value.tag.identifier.id)) ||
|
||||||
(value.tag.kind === "BuiltinTag" && FBT_TAGS.has(value.tag.name)))
|
(value.tag.kind === "BuiltinTag" && fbtMacroTags.has(value.tag.name)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclara
|
||||||
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
|
export { flattenReactiveLoops } from "./FlattenReactiveLoops";
|
||||||
export { flattenScopesWithHooksOrUse } from "./FlattenScopesWithHooksOrUse";
|
export { flattenScopesWithHooksOrUse } from "./FlattenScopesWithHooksOrUse";
|
||||||
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
|
export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
|
||||||
export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
|
export { memoizeFbtAndMacroOperandsInSameScope as memoizeFbtOperandsInSameScope } from "./MemoizeFbtAndMacroOperandsInSameScope";
|
||||||
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
|
export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
|
||||||
export { mergeReactiveScopesThatInvalidateTogether } from "./MergeReactiveScopesThatInvalidateTogether";
|
export { mergeReactiveScopesThatInvalidateTogether } from "./MergeReactiveScopesThatInvalidateTogether";
|
||||||
export { printReactiveFunction } from "./PrintReactiveFunction";
|
export { printReactiveFunction } from "./PrintReactiveFunction";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
|
||||||
|
import { identity } from "shared-runtime";
|
||||||
|
|
||||||
|
const DARK = "dark";
|
||||||
|
|
||||||
|
function Component() {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx({
|
||||||
|
"styles/light": true,
|
||||||
|
"styles/dark": theme.getTheme() === DARK,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cx(obj) {
|
||||||
|
const classes = [];
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value) {
|
||||||
|
classes.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTheme() {
|
||||||
|
return {
|
||||||
|
getTheme() {
|
||||||
|
return DARK;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
|
||||||
|
import { identity } from "shared-runtime";
|
||||||
|
|
||||||
|
const DARK = "dark";
|
||||||
|
|
||||||
|
function Component() {
|
||||||
|
const $ = _c(2);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const t0 = cx({
|
||||||
|
"styles/light": true,
|
||||||
|
"styles/dark": theme.getTheme() === DARK,
|
||||||
|
});
|
||||||
|
let t1;
|
||||||
|
if ($[0] !== t0) {
|
||||||
|
t1 = <div className={t0} />;
|
||||||
|
$[0] = t0;
|
||||||
|
$[1] = t1;
|
||||||
|
} else {
|
||||||
|
t1 = $[1];
|
||||||
|
}
|
||||||
|
return t1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cx(obj) {
|
||||||
|
const classes = [];
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value) {
|
||||||
|
classes.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTheme() {
|
||||||
|
return {
|
||||||
|
getTheme() {
|
||||||
|
return DARK;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eval output
|
||||||
|
(kind: ok) <div class="styles/light styles/dark"></div>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// @compilationMode(infer) @enableAssumeHooksFollowRulesOfReact:false @customMacros(cx)
|
||||||
|
import { identity } from "shared-runtime";
|
||||||
|
|
||||||
|
const DARK = "dark";
|
||||||
|
|
||||||
|
function Component() {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx({
|
||||||
|
"styles/light": true,
|
||||||
|
"styles/dark": theme.getTheme() === DARK,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cx(obj) {
|
||||||
|
const classes = [];
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value) {
|
||||||
|
classes.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTheme() {
|
||||||
|
return {
|
||||||
|
getTheme() {
|
||||||
|
return DARK;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{}],
|
||||||
|
};
|
||||||
|
|
@ -46,6 +46,7 @@ function makePluginOptions(
|
||||||
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
||||||
let validatePreserveExistingMemoizationGuarantees = false;
|
let validatePreserveExistingMemoizationGuarantees = false;
|
||||||
let enableChangeDetectionForDebugging = null;
|
let enableChangeDetectionForDebugging = null;
|
||||||
|
let customMacros = null;
|
||||||
|
|
||||||
if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
|
if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -142,6 +143,18 @@ function makePluginOptions(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customMacrosMatch = /@customMacros\(([^)]+)\)/.exec(firstLine);
|
||||||
|
if (
|
||||||
|
customMacrosMatch &&
|
||||||
|
customMacrosMatch.length > 1 &&
|
||||||
|
customMacrosMatch[1].trim().length > 0
|
||||||
|
) {
|
||||||
|
customMacros = customMacrosMatch[1]
|
||||||
|
.split(" ")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
let logs: Array<{ filename: string | null; event: LoggerEvent }> = [];
|
let logs: Array<{ filename: string | null; event: LoggerEvent }> = [];
|
||||||
let logger: Logger | null = null;
|
let logger: Logger | null = null;
|
||||||
if (firstLine.includes("@logger")) {
|
if (firstLine.includes("@logger")) {
|
||||||
|
|
@ -185,6 +198,7 @@ function makePluginOptions(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
customMacros,
|
||||||
enableEmitFreeze,
|
enableEmitFreeze,
|
||||||
enableEmitInstrumentForget,
|
enableEmitInstrumentForget,
|
||||||
enableEmitHookGuards,
|
enableEmitHookGuards,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user