[compiler][ez] Clean up pragma parsing for tests + playground (#31347)
Move environment config parsing for `inlineJsxTransform`, `lowerContextAccess`, and some dev-only options out of snap (test fixture). These should now be available for playground via `@inlineJsxTransform` and `lowerContextAccess`. Other small change: Changed zod fields from `nullish()` -> `nullable().default(null)`. [`nullish`](https://zod.dev/?id=nullish) fields accept `null | undefined` and default to `undefined`. We don't distinguish between null and undefined for any of these options, so let's only accept null + default to null. This also makes EnvironmentConfig in the playground more accurate. Previously, some fields just didn't show up as `prettyFormat({field: undefined})` does not print `field`.
mofeiZ committed
Nov 5, 2024 at 18:19 UTC
792fa065ca7a46ce4a583e8f6f35eec8bd813d43
15 files changed
+101
-114
compiler/apps/playground/components/Editor/EditorImpl.tsx
+2
-2
@@ -14,7 +14,7 @@ import {
14
CompilerErrorDetail,
15
Effect,
16
ErrorSeverity,
17
- parseConfigPragma,
17
+ parseConfigPragmaForTests,
18
ValueKind,
19
runPlayground,
20
type Hook,
@@ -208,7 +208,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
208
try {
209
// Extract the first line to quickly check for custom test directives
210
const pragma = source.substring(0, source.indexOf('\n'));
211
- const config = parseConfigPragma(pragma);
211
+ const config = parseConfigPragmaForTests(pragma);
212
213
for (const fn of parseFunctions(source, language)) {
214
const id = withIdentifier(getFunctionIdentifier(fn));
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+64
-31
@@ -69,8 +69,8 @@ export const ExternalFunctionSchema = z.object({
69
export const InstrumentationSchema = z
70
.object({
71
fn: ExternalFunctionSchema,
72
- gating: ExternalFunctionSchema.nullish(),
73
- globalGating: z.string().nullish(),
72
+ gating: ExternalFunctionSchema.nullable(),
73
+ globalGating: z.string().nullable(),
74
})
75
.refine(
76
opts => opts.gating != null || opts.globalGating != null,
@@ -147,7 +147,7 @@ export type Hook = z.infer<typeof HookSchema>;
147
*/
148
149
const EnvironmentConfigSchema = z.object({
150
- customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
150
+ customHooks: z.map(z.string(), HookSchema).default(new Map()),
151
152
/**
153
* A function that, given the name of a module, can optionally return a description
@@ -248,7 +248,7 @@ const EnvironmentConfigSchema = z.object({
248
*
249
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
250
*/
251
- inlineJsxTransform: ReactElementSymbolSchema.nullish(),
251
+ inlineJsxTransform: ReactElementSymbolSchema.nullable().default(null),
252
253
/*
254
* Enable validation of hooks to partially check that the component honors the rules of hooks.
@@ -339,9 +339,9 @@ const EnvironmentConfigSchema = z.object({
339
* }
340
* }
341
*/
342
- enableEmitFreeze: ExternalFunctionSchema.nullish(),
342
+ enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
343
344
- enableEmitHookGuards: ExternalFunctionSchema.nullish(),
344
+ enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),
345
346
/**
347
* Enable instruction reordering. See InstructionReordering.ts for the details
@@ -425,7 +425,7 @@ const EnvironmentConfigSchema = z.object({
425
* }
426
*
427
*/
428
- enableEmitInstrumentForget: InstrumentationSchema.nullish(),
428
+ enableEmitInstrumentForget: InstrumentationSchema.nullable().default(null),
429
430
// Enable validation of mutable ranges
431
assertValidMutableRanges: z.boolean().default(false),
@@ -464,8 +464,6 @@ const EnvironmentConfigSchema = z.object({
464
*/
465
throwUnknownException__testonly: z.boolean().default(false),
466
467
- enableSharedRuntime__testonly: z.boolean().default(false),
468
-
467
/**
468
* Enables deps of a function epxression to be treated as conditional. This
469
* makes sure we don't load a dep when it's a property (to check if it has
@@ -503,7 +501,8 @@ const EnvironmentConfigSchema = z.object({
501
* computed one. This detects cases where rules of react violations may cause the
502
* compiled code to behave differently than the original.
503
*/
506
- enableChangeDetectionForDebugging: ExternalFunctionSchema.nullish(),
504
+ enableChangeDetectionForDebugging:
505
+ ExternalFunctionSchema.nullable().default(null),
506
507
/**
508
* The react native re-animated library uses custom Babel transforms that
@@ -543,7 +542,7 @@ const EnvironmentConfigSchema = z.object({
542
*
543
* Here the variables `ref` and `myRef` will be typed as Refs.
544
*/
546
- enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
545
+ enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
546
547
/*
548
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -565,12 +564,57 @@ const EnvironmentConfigSchema = z.object({
564
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
565
* ```
566
*/
568
- lowerContextAccess: ExternalFunctionSchema.nullish(),
567
+ lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
568
});
569
570
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
571
573
-export function parseConfigPragma(pragma: string): EnvironmentConfig {
572
+/**
573
+ * For test fixtures and playground only.
574
+ *
575
+ * Pragmas are straightforward to parse for boolean options (`:true` and
576
+ * `:false`). These are 'enabled' config values for non-boolean configs (i.e.
577
+ * what is used when parsing `:true`).
578
+ */
579
+const testComplexConfigDefaults: PartialEnvironmentConfig = {
580
+ validateNoCapitalizedCalls: [],
581
+ enableChangeDetectionForDebugging: {
582
+ source: 'react-compiler-runtime',
583
+ importSpecifierName: '$structuralCheck',
584
+ },
585
+ enableEmitFreeze: {
586
+ source: 'react-compiler-runtime',
587
+ importSpecifierName: 'makeReadOnly',
588
+ },
589
+ enableEmitInstrumentForget: {
590
+ fn: {
591
+ source: 'react-compiler-runtime',
592
+ importSpecifierName: 'useRenderCounter',
593
+ },
594
+ gating: {
595
+ source: 'react-compiler-runtime',
596
+ importSpecifierName: 'shouldInstrument',
597
+ },
598
+ globalGating: '__DEV__',
599
+ },
600
+ enableEmitHookGuards: {
601
+ source: 'react-compiler-runtime',
602
+ importSpecifierName: '$dispatcherGuard',
603
+ },
604
+ inlineJsxTransform: {
605
+ elementSymbol: 'react.transitional.element',
606
+ globalDevVar: 'DEV',
607
+ },
608
+ lowerContextAccess: {
609
+ source: 'react-compiler-runtime',
610
+ importSpecifierName: 'useContext_withSelector',
611
+ },
612
+};
613
+
614
+/**
615
+ * For snap test fixtures and playground only.
616
+ */
617
+export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
618
const maybeConfig: any = {};
619
// Get the defaults to programmatically check for boolean properties
620
const defaultConfig = EnvironmentConfigSchema.parse({});
@@ -580,21 +624,12 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
624
continue;
625
}
626
const keyVal = token.slice(1);
583
- let [key, val]: any = keyVal.split(':');
584
-
585
- if (key === 'validateNoCapitalizedCalls') {
586
- maybeConfig[key] = [];
587
- continue;
588
- }
627
+ let [key, val = undefined] = keyVal.split(':');
628
+ const isSet = val === undefined || val === 'true';
629
590
- if (
591
- key === 'enableChangeDetectionForDebugging' &&
592
- (val === undefined || val === 'true')
593
- ) {
594
- maybeConfig[key] = {
595
- source: 'react-compiler-runtime',
596
- importSpecifierName: '$structuralCheck',
597
- };
630
+ if (isSet && key in testComplexConfigDefaults) {
631
+ maybeConfig[key] =
632
+ testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
633
continue;
634
}
635
@@ -609,7 +644,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
644
props.push({type: 'name', name: elt});
645
}
646
}
612
- console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
647
maybeConfig[key] = [[valSplit[0], props]];
648
}
649
continue;
@@ -620,11 +654,10 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
654
continue;
655
}
656
if (val === undefined || val === 'true') {
623
- val = true;
657
+ maybeConfig[key] = true;
658
} else {
625
- val = false;
659
+ maybeConfig[key] = false;
660
}
627
- maybeConfig[key] = val;
661
}
662
663
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts
+1
-1
@@ -17,7 +17,7 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
17
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
18
export {
19
Environment,
20
- parseConfigPragma,
20
+ parseConfigPragmaForTests,
21
validateEnvironmentConfig,
22
type EnvironmentConfig,
23
type ExternalFunction,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @enableEmitFreeze @instrumentForget
5
+// @enableEmitFreeze @enableEmitInstrumentForget
6
7
function useFoo(props) {
8
return foo(props.x);
@@ -18,7 +18,7 @@ import {
18
shouldInstrument,
19
makeReadOnly,
20
} from "react-compiler-runtime";
21
-import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
21
+import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @enableEmitInstrumentForget
22
23
function useFoo(props) {
24
if (__DEV__ && shouldInstrument)
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @enableEmitFreeze @instrumentForget
1
+// @enableEmitFreeze @enableEmitInstrumentForget
2
3
function useFoo(props) {
4
return foo(props.x);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @instrumentForget @compilationMode(annotation) @gating
5
+// @enableEmitInstrumentForget @compilationMode(annotation) @gating
6
7
function Bar(props) {
8
'use forget';
@@ -25,7 +25,7 @@ function Foo(props) {
25
```javascript
26
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
27
import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
28
-import { c as _c } from "react/compiler-runtime"; // @instrumentForget @compilationMode(annotation) @gating
28
+import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
29
const Bar = isForgetEnabled_Fixtures()
30
? function Bar(props) {
31
"use forget";
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @instrumentForget @compilationMode(annotation) @gating
1
+// @enableEmitInstrumentForget @compilationMode(annotation) @gating
2
3
function Bar(props) {
4
'use forget';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @instrumentForget @compilationMode(annotation)
5
+// @enableEmitInstrumentForget @compilationMode(annotation)
6
7
function Bar(props) {
8
'use forget';
@@ -24,7 +24,7 @@ function Foo(props) {
24
25
```javascript
26
import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
27
-import { c as _c } from "react/compiler-runtime"; // @instrumentForget @compilationMode(annotation)
27
+import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
28
29
function Bar(props) {
30
"use forget";
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @instrumentForget @compilationMode(annotation)
1
+// @enableEmitInstrumentForget @compilationMode(annotation)
2
3
function Bar(props) {
4
'use forget';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @enableInlineJsxTransform
5
+// @inlineJsxTransform
6
7
function Parent({children, a: _a, b: _b, c: _c, ref}) {
8
return <div ref={ref}>{children}</div>;
@@ -76,7 +76,7 @@ export const FIXTURE_ENTRYPOINT = {
76
## Code
77
78
```javascript
79
-import { c as _c2 } from "react/compiler-runtime"; // @enableInlineJsxTransform
79
+import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
80
81
function Parent(t0) {
82
const $ = _c2(2);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @enableInlineJsxTransform
1
+// @inlineJsxTransform
2
3
function Parent({children, a: _a, b: _b, c: _c, ref}) {
4
return <div ref={ref}>{children}</div>;
compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts
+3
-3
@@ -5,9 +5,9 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {parseConfigPragma, validateEnvironmentConfig} from '..';
8
+import {parseConfigPragmaForTests, validateEnvironmentConfig} from '..';
9
10
-describe('parseConfigPragma()', () => {
10
+describe('parseConfigPragmaForTests()', () => {
11
it('parses flags in various forms', () => {
12
const defaultConfig = validateEnvironmentConfig({});
13
@@ -17,7 +17,7 @@ describe('parseConfigPragma()', () => {
17
expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false);
18
expect(defaultConfig.validateNoSetStateInRender).toBe(true);
19
20
- const config = parseConfigPragma(
20
+ const config = parseConfigPragmaForTests(
21
'@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false',
22
);
23
expect(config).toEqual({
compiler/packages/babel-plugin-react-compiler/src/index.ts
+1
-1
@@ -26,7 +26,7 @@ export {
26
export {
27
Effect,
28
ValueKind,
29
- parseConfigPragma,
29
+ parseConfigPragmaForTests,
30
printHIR,
31
validateEnvironmentConfig,
32
type EnvironmentConfig,
compiler/packages/snap/src/compiler.ts
+14
-60
@@ -21,10 +21,9 @@ import type {
21
} from 'babel-plugin-react-compiler/src/Entrypoint';
22
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
23
import type {
24
- EnvironmentConfig,
24
Macro,
25
MacroMethod,
27
- parseConfigPragma as ParseConfigPragma,
26
+ parseConfigPragmaForTests as ParseConfigPragma,
27
} from 'babel-plugin-react-compiler/src/HIR/Environment';
28
import * as HermesParser from 'hermes-parser';
29
import invariant from 'invariant';
@@ -37,6 +36,11 @@ export function parseLanguage(source: string): 'flow' | 'typescript' {
36
return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
37
}
38
39
+/**
40
+ * Parse react compiler plugin + environment options from test fixture. Note
41
+ * that although this primarily uses `Environment:parseConfigPragma`, it also
42
+ * has test fixture specific (i.e. not applicable to playground) parsing logic.
43
+ */
44
function makePluginOptions(
45
firstLine: string,
46
parseConfigPragmaFn: typeof ParseConfigPragma,
@@ -44,15 +48,11 @@ function makePluginOptions(
48
ValueKindEnum: typeof ValueKind,
49
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
50
let gating = null;
47
- let enableEmitInstrumentForget = null;
48
- let enableEmitFreeze = null;
49
- let enableEmitHookGuards = null;
51
let compilationMode: CompilationMode = 'all';
52
let panicThreshold: PanicThresholdOptions = 'all_errors';
53
let hookPattern: string | null = null;
54
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
55
let validatePreserveExistingMemoizationGuarantees = false;
55
- let enableChangeDetectionForDebugging = null;
56
let customMacros: null | Array<Macro> = null;
57
let validateBlocklistedImports = null;
58
let target = '19' as const;
@@ -78,31 +78,6 @@ function makePluginOptions(
78
importSpecifierName: 'isForgetEnabled_Fixtures',
79
};
80
}
81
- if (firstLine.includes('@instrumentForget')) {
82
- enableEmitInstrumentForget = {
83
- fn: {
84
- source: 'react-compiler-runtime',
85
- importSpecifierName: 'useRenderCounter',
86
- },
87
- gating: {
88
- source: 'react-compiler-runtime',
89
- importSpecifierName: 'shouldInstrument',
90
- },
91
- globalGating: '__DEV__',
92
- };
93
- }
94
- if (firstLine.includes('@enableEmitFreeze')) {
95
- enableEmitFreeze = {
96
- source: 'react-compiler-runtime',
97
- importSpecifierName: 'makeReadOnly',
98
- };
99
- }
100
- if (firstLine.includes('@enableEmitHookGuards')) {
101
- enableEmitHookGuards = {
102
- source: 'react-compiler-runtime',
103
- importSpecifierName: '$dispatcherGuard',
104
- };
105
- }
81
82
const targetMatch = /@target="([^"]+)"/.exec(firstLine);
83
if (targetMatch) {
@@ -132,16 +107,18 @@ function makePluginOptions(
107
ignoreUseNoForget = true;
108
}
109
110
+ /**
111
+ * Snap currently runs all fixtures without `validatePreserveExistingMemo` as
112
+ * most fixtures are interested in compilation output, not whether the
113
+ * compiler was able to preserve existing memo.
114
+ *
115
+ * TODO: flip the default. `useMemo` is rare in test fixtures -- fixtures that
116
+ * use useMemo should be explicit about whether this flag is enabled
117
+ */
118
if (firstLine.includes('@validatePreserveExistingMemoizationGuarantees')) {
119
validatePreserveExistingMemoizationGuarantees = true;
120
}
121
139
- if (firstLine.includes('@enableChangeDetectionForDebugging')) {
140
- enableChangeDetectionForDebugging = {
141
- source: 'react-compiler-runtime',
142
- importSpecifierName: '$structuralCheck',
143
- };
144
- }
122
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
123
if (
124
hookPatternMatch &&
@@ -197,22 +174,6 @@ function makePluginOptions(
174
.filter(s => s.length > 0);
175
}
176
200
- let lowerContextAccess = null;
201
- if (firstLine.includes('@lowerContextAccess')) {
202
- lowerContextAccess = {
203
- source: 'react-compiler-runtime',
204
- importSpecifierName: 'useContext_withSelector',
205
- };
206
- }
207
-
208
- let inlineJsxTransform: EnvironmentConfig['inlineJsxTransform'] = null;
209
- if (firstLine.includes('@enableInlineJsxTransform')) {
210
- inlineJsxTransform = {
211
- elementSymbol: 'react.transitional.element',
212
- globalDevVar: 'DEV',
213
- };
214
- }
215
-
177
let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
178
let logger: Logger | null = null;
179
if (firstLine.includes('@logger')) {
@@ -232,17 +193,10 @@ function makePluginOptions(
193
ValueKindEnum,
194
}),
195
customMacros,
235
- enableEmitFreeze,
236
- enableEmitInstrumentForget,
237
- enableEmitHookGuards,
196
assertValidMutableRanges: true,
239
- enableSharedRuntime__testonly: true,
197
hookPattern,
198
validatePreserveExistingMemoizationGuarantees,
242
- enableChangeDetectionForDebugging,
243
- lowerContextAccess,
199
validateBlocklistedImports,
245
- inlineJsxTransform,
200
},
201
compilationMode,
202
logger,
compiler/packages/snap/src/runner-worker.ts
+4
-4
@@ -7,7 +7,7 @@
7
8
import {codeFrameColumns} from '@babel/code-frame';
9
import type {PluginObj} from '@babel/core';
10
-import type {parseConfigPragma as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment';
10
+import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment';
11
import {TransformResult, transformFixtureInput} from './compiler';
12
import {
13
COMPILER_PATH,
@@ -65,8 +65,8 @@ async function compile(
65
COMPILER_INDEX_PATH,
66
);
67
const {toggleLogging} = require(LOGGER_PATH);
68
- const {parseConfigPragma} = require(PARSE_CONFIG_PRAGMA_PATH) as {
69
- parseConfigPragma: typeof ParseConfigPragma;
68
+ const {parseConfigPragmaForTests} = require(PARSE_CONFIG_PRAGMA_PATH) as {
69
+ parseConfigPragmaForTests: typeof ParseConfigPragma;
70
};
71
72
// only try logging if we filtered out all but one fixture,
@@ -75,7 +75,7 @@ async function compile(
75
const result = await transformFixtureInput(
76
input,
77
fixturePath,
78
- parseConfigPragma,
78
+ parseConfigPragmaForTests,
79
BabelPluginReactCompiler,
80
includeEvaluator,
81
EffectEnum,