@samitouri / QOS-React-2 / commits / e30872a4e0

[compiler][be] Playground now compiles entire program (#31774)

Compiler playground now runs the entire program through `babel-plugin-react-compiler` instead of a custom pipeline which previously duplicated function inference logic from `Program.ts`. In addition, the playground output reflects the tranformed file (instead of a "virtual file" of manually concatenated functions). This helps with the following: - Reduce potential discrepencies between playground and babel plugin behavior. See attached fixture output for an example where we previously diverged. - Let playground users see compiler-inserted imports (e.g. `_c` or `useFire`) This also helps us repurpose playground into a more general tool for compiler-users instead of just for compiler engineers. - imports and other functions are preserved. We differentiate between imports and globals in many cases (e.g. `inferEffectDeps`), so it may be misleading to omit imports in printed output - playground now shows other program-changing behavior like position of outlined functions and hoisted declarations - emitted compiled functions do not need synthetic names --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31774). * #31809 * __->__ #31774

mofeiZ committed Dec 16, 2024 at 14:43 UTC e30872a4e01bdc0cf185a818156ae7741c815e21
20 files changed +302 -343
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/01-user-output.txt
+2 -1
@@ -1,4 +1,5 @@
1 -function TestComponent(t0) {
1 +import { c as _c } from "react/compiler-runtime";
2 +export default function TestComponent(t0) {
3 const $ = _c(2);
4 const { x } = t0;
5 let t1;
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/02-default-output.txt
+2 -1
@@ -1,4 +1,5 @@
1 -function MyApp() {
1 +import { c as _c } from "react/compiler-runtime";
2 +export default function MyApp() {
3 const $ = _c(1);
4 let t0;
5 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-memo-output.txt
+3 -1
@@ -1,4 +1,6 @@
1 -function TestComponent(t0) {
1 +"use memo";
2 +import { c as _c } from "react/compiler-runtime";
3 +export default function TestComponent(t0) {
4 const $ = _c(2);
5 const { x } = t0;
6 let t1;
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-output.txt
+2 -1
@@ -1,3 +1,4 @@
1 -function TestComponent({ x }) {
1 +"use no memo";
2 +export default function TestComponent({ x }) {
3 return <Button>{x}</Button>;
4 }
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/parse-flow-output.txt new
+14
@@ -0,0 +1,14 @@
1 +import { c as _c } from "react/compiler-runtime";
2 +function useFoo(propVal) {
3 +  const $ = _c(2);
4 +  const t0 = (propVal.baz: number);
5 +  let t1;
6 +  if ($[0] !== t0) {
7 +    t1 = <div>{t0}</div>;
8 +    $[0] = t0;
9 +    $[1] = t1;
10 +  } else {
11 +    t1 = $[1];
12 +  }
13 +  return t1;
14 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/parse-typescript-output.txt new
+20
@@ -0,0 +1,20 @@
1 +import { c as _c } from "react/compiler-runtime";
2 +function Foo() {
3 +  const $ = _c(2);
4 +  let t0;
5 +  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
6 +    t0 = foo();
7 +    $[0] = t0;
8 +  } else {
9 +    t0 = $[0];
10 +  }
11 +  const x = t0 as number;
12 +  let t1;
13 +  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
14 +    t1 = <div>{x}</div>;
15 +    $[1] = t1;
16 +  } else {
17 +    t1 = $[1];
18 +  }
19 +  return t1;
20 +}
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/todo-function-scope-does-not-beat-module-scope-output.txt new
+5
@@ -0,0 +1,5 @@
1 +"use no memo";
2 +function TestComponent({ x }) {
3 + "use memo";
4 + return <Button>{x}</Button>;
5 +}
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/use-memo-output.txt
+3 -2
@@ -1,3 +1,4 @@
1 +import { c as _c } from "react/compiler-runtime";
2 function TestComponent(t0) {
3 "use memo";
4 const $ = _c(2);
@@ -12,7 +13,7 @@ function TestComponent(t0) {
13 }
14 return t1;
15 }
15 -function anonymous_1(t0) {
16 +const TestComponent2 = (t0) => {
17 "use memo";
18 const $ = _c(2);
19 const { x } = t0;
@@ -25,4 +26,4 @@ function anonymous_1(t0) {
26 t1 = $[1];
27 }
28 return t1;
28 -}
29 +};
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/use-no-memo-output.txt
+4 -4
@@ -1,8 +1,8 @@
1 -function anonymous_1() {
1 +const TestComponent = function () {
2 "use no memo";
3 return <Button>{x}</Button>;
4 -}
5 -function anonymous_3({ x }) {
4 +};
5 +const TestComponent2 = ({ x }) => {
6 "use no memo";
7 return <Button>{x}</Button>;
8 -}
8 +};
compiler/apps/playground/__tests__/e2e/page.spec.ts
+33 -8
@@ -9,11 +9,11 @@ import {expect, test} from '@playwright/test';
9 import {encodeStore, type Store} from '../../lib/stores';
10 import {format} from 'prettier';
11
12 -function print(data: Array<string>): Promise<string> {
12 +function formatPrint(data: Array<string>): Promise<string> {
13 return format(data.join(''), {parser: 'babel'});
14 }
15
16 -const DIRECTIVE_TEST_CASES = [
16 +const TEST_CASE_INPUTS = [
17 {
18 name: 'module-scope-use-memo',
19 input: `
@@ -55,7 +55,7 @@ const TestComponent2 = ({ x }) => {
55 };`,
56 },
57 {
58 - name: 'function-scope-beats-module-scope',
58 + name: 'todo-function-scope-does-not-beat-module-scope',
59 input: `
60 'use no memo';
61 function TestComponent({ x }) {
@@ -63,6 +63,26 @@ function TestComponent({ x }) {
63 return <Button>{x}</Button>;
64 }`,
65 },
66 + {
67 + name: 'parse-typescript',
68 + input: `
69 +function Foo() {
70 + const x = foo() as number;
71 + return <div>{x}</div>;
72 +}
73 +`,
74 + noFormat: true,
75 + },
76 + {
77 + name: 'parse-flow',
78 + input: `
79 +// @flow
80 +function useFoo(propVal: {+baz: number}) {
81 + return <div>{(propVal.baz as number)}</div>;
82 +}
83 + `,
84 + noFormat: true,
85 + },
86 ];
87
88 test('editor should open successfully', async ({page}) => {
@@ -90,7 +110,7 @@ test('editor should compile from hash successfully', async ({page}) => {
110 });
111 const text =
112 (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
93 - const output = await print(text);
113 + const output = await formatPrint(text);
114
115 expect(output).not.toEqual('');
116 expect(output).toMatchSnapshot('01-user-output.txt');
@@ -115,14 +135,14 @@ test('reset button works', async ({page}) => {
135 });
136 const text =
137 (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
118 - const output = await print(text);
138 + const output = await formatPrint(text);
139
140 expect(output).not.toEqual('');
141 expect(output).toMatchSnapshot('02-default-output.txt');
142 });
143
124 -DIRECTIVE_TEST_CASES.forEach((t, idx) =>
125 - test(`directives work: ${t.name}`, async ({page}) => {
144 +TEST_CASE_INPUTS.forEach((t, idx) =>
145 + test(`playground compiles: ${t.name}`, async ({page}) => {
146 const store: Store = {
147 source: t.input,
148 };
@@ -135,7 +155,12 @@ DIRECTIVE_TEST_CASES.forEach((t, idx) =>
155
156 const text =
157 (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
138 - const output = await print(text);
158 + let output: string;
159 + if (t.noFormat) {
160 + output = text.join('');
161 + } else {
162 + output = await formatPrint(text);
163 + }
164
165 expect(output).not.toEqual('');
166 expect(output).toMatchSnapshot(`${t.name}-output.txt`);
compiler/apps/playground/components/Editor/EditorImpl.tsx
+59 -201
@@ -5,23 +5,22 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {parse as babelParse} from '@babel/parser';
8 +import {parse as babelParse, ParseResult} from '@babel/parser';
9 import * as HermesParser from 'hermes-parser';
10 -import traverse, {NodePath} from '@babel/traverse';
10 import * as t from '@babel/types';
12 -import {
11 +import BabelPluginReactCompiler, {
12 CompilerError,
13 CompilerErrorDetail,
14 Effect,
15 ErrorSeverity,
16 parseConfigPragmaForTests,
17 ValueKind,
19 - runPlayground,
18 type Hook,
21 - findDirectiveDisablingMemoization,
22 - findDirectiveEnablingMemoization,
19 + PluginOptions,
20 + CompilerPipelineValue,
21 + parsePluginOptions,
22 } from 'babel-plugin-react-compiler/src';
24 -import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
23 +import {type EnvironmentConfig} from 'babel-plugin-react-compiler/src/HIR/Environment';
24 import clsx from 'clsx';
25 import invariant from 'invariant';
26 import {useSnackbar} from 'notistack';
@@ -39,32 +38,18 @@ import {useStore, useStoreDispatch} from '../StoreContext';
38 import Input from './Input';
39 import {
40 CompilerOutput,
41 + CompilerTransformOutput,
42 default as Output,
43 PrintedCompilerPipelineValue,
44 } from './Output';
45 import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
46 import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
47 +import {transformFromAstSync} from '@babel/core';
48
48 -type FunctionLike =
49 - | NodePath<t.FunctionDeclaration>
50 - | NodePath<t.ArrowFunctionExpression>
51 - | NodePath<t.FunctionExpression>;
52 -enum MemoizeDirectiveState {
53 - Enabled = 'Enabled',
54 - Disabled = 'Disabled',
55 - Undefined = 'Undefined',
56 -}
57 -
58 -const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
59 - MemoizeDirectiveState.Enabled,
60 - MemoizeDirectiveState.Undefined,
61 -]);
62 -
63 -const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
64 - MemoizeDirectiveState.Enabled,
65 - MemoizeDirectiveState.Disabled,
66 -]);
67 -function parseInput(input: string, language: 'flow' | 'typescript'): any {
49 +function parseInput(
50 + input: string,
51 + language: 'flow' | 'typescript',
52 +): ParseResult<t.File> {
53 // Extract the first line to quickly check for custom test directives
54 if (language === 'flow') {
55 return HermesParser.parse(input, {
@@ -77,95 +62,45 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
62 return babelParse(input, {
63 plugins: ['typescript', 'jsx'],
64 sourceType: 'module',
80 - });
65 + }) as ParseResult<t.File>;
66 }
67 }
68
84 -function parseFunctions(
69 +function invokeCompiler(
70 source: string,
71 language: 'flow' | 'typescript',
87 -): Array<{
88 - compilationEnabled: boolean;
89 - fn: FunctionLike;
90 -}> {
91 - const items: Array<{
92 - compilationEnabled: boolean;
93 - fn: FunctionLike;
94 - }> = [];
95 - try {
96 - const ast = parseInput(source, language);
97 - traverse(ast, {
98 - FunctionDeclaration(nodePath) {
99 - items.push({
100 - compilationEnabled: shouldCompile(nodePath),
101 - fn: nodePath,
102 - });
103 - nodePath.skip();
104 - },
105 - ArrowFunctionExpression(nodePath) {
106 - items.push({
107 - compilationEnabled: shouldCompile(nodePath),
108 - fn: nodePath,
109 - });
110 - nodePath.skip();
111 - },
112 - FunctionExpression(nodePath) {
113 - items.push({
114 - compilationEnabled: shouldCompile(nodePath),
115 - fn: nodePath,
116 - });
117 - nodePath.skip();
118 - },
119 - });
120 - } catch (e) {
121 - console.error(e);
122 - CompilerError.throwInvalidJS({
123 - reason: String(e),
124 - description: null,
125 - loc: null,
126 - suggestions: null,
127 - });
128 - }
129 -
130 - return items;
131 -}
132 -
133 -function shouldCompile(fn: FunctionLike): boolean {
134 - const {body} = fn.node;
135 - if (t.isBlockStatement(body)) {
136 - const selfCheck = checkExplicitMemoizeDirectives(body.directives);
137 - if (selfCheck === MemoizeDirectiveState.Enabled) return true;
138 - if (selfCheck === MemoizeDirectiveState.Disabled) return false;
139 -
140 - const parentWithDirective = fn.findParent(parentPath => {
141 - if (parentPath.isBlockStatement() || parentPath.isProgram()) {
142 - const directiveCheck = checkExplicitMemoizeDirectives(
143 - parentPath.node.directives,
144 - );
145 - return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
146 - }
147 - return false;
148 - });
149 -
150 - if (!parentWithDirective) return true;
151 - const parentDirectiveCheck = checkExplicitMemoizeDirectives(
152 - (parentWithDirective.node as t.Program | t.BlockStatement).directives,
153 - );
154 - return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
155 - }
156 - return false;
157 -}
158 -
159 -function checkExplicitMemoizeDirectives(
160 - directives: Array<t.Directive>,
161 -): MemoizeDirectiveState {
162 - if (findDirectiveEnablingMemoization(directives).length) {
163 - return MemoizeDirectiveState.Enabled;
164 - }
165 - if (findDirectiveDisablingMemoization(directives).length) {
166 - return MemoizeDirectiveState.Disabled;
72 + environment: EnvironmentConfig,
73 + logIR: (pipelineValue: CompilerPipelineValue) => void,
74 +): CompilerTransformOutput {
75 + const opts: PluginOptions = parsePluginOptions({
76 + logger: {
77 + debugLogIRs: logIR,
78 + logEvent: () => {},
79 + },
80 + environment,
81 + compilationMode: 'all',
82 + panicThreshold: 'all_errors',
83 + });
84 + const ast = parseInput(source, language);
85 + let result = transformFromAstSync(ast, source, {
86 + filename: '_playgroundFile.js',
87 + highlightCode: false,
88 + retainLines: true,
89 + plugins: [[BabelPluginReactCompiler, opts]],
90 + ast: true,
91 + sourceType: 'module',
92 + configFile: false,
93 + sourceMaps: true,
94 + babelrc: false,
95 + });
96 + if (result?.ast == null || result?.code == null || result?.map == null) {
97 + throw new Error('Expected successful compilation');
98 }
168 - return MemoizeDirectiveState.Undefined;
99 + return {
100 + code: result.code,
101 + sourceMaps: result.map,
102 + language,
103 + };
104 }
105
106 const COMMON_HOOKS: Array<[string, Hook]> = [
@@ -216,37 +151,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
151 ],
152 ];
153
219 -function isHookName(s: string): boolean {
220 - return /^use[A-Z0-9]/.test(s);
221 -}
222 -
223 -function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
224 - if (id != null) {
225 - if (isHookName(id.name)) {
226 - return 'Hook';
227 - }
228 -
229 - const isPascalCaseNameSpace = /^[A-Z].*/;
230 - if (isPascalCaseNameSpace.test(id.name)) {
231 - return 'Component';
232 - }
233 - }
234 - return 'Other';
235 -}
236 -
237 -function getFunctionIdentifier(
238 - fn:
239 - | NodePath<t.FunctionDeclaration>
240 - | NodePath<t.ArrowFunctionExpression>
241 - | NodePath<t.FunctionExpression>,
242 -): t.Identifier | null {
243 - if (fn.isArrowFunctionExpression()) {
244 - return null;
245 - }
246 - const id = fn.get('id');
247 - return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
248 -}
249 -
154 function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
155 const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
156 const error = new CompilerError();
@@ -264,71 +168,25 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
168 } else {
169 language = 'typescript';
170 }
267 - let count = 0;
268 - const withIdentifier = (id: t.Identifier | null): t.Identifier => {
269 - if (id != null && id.name != null) {
270 - return id;
271 - } else {
272 - return t.identifier(`anonymous_${count++}`);
273 - }
274 - };
171 + let transformOutput;
172 try {
173 // Extract the first line to quickly check for custom test directives
174 const pragma = source.substring(0, source.indexOf('\n'));
175 const config = parseConfigPragmaForTests(pragma);
279 - const parsedFunctions = parseFunctions(source, language);
280 - for (const func of parsedFunctions) {
281 - const id = withIdentifier(getFunctionIdentifier(func.fn));
282 - const fnName = id.name;
283 - if (!func.compilationEnabled) {
284 - upsert({
285 - kind: 'ast',
286 - fnName,
287 - name: 'CodeGen',
288 - value: {
289 - type: 'FunctionDeclaration',
290 - id:
291 - func.fn.isArrowFunctionExpression() ||
292 - func.fn.isFunctionExpression()
293 - ? withIdentifier(null)
294 - : func.fn.node.id,
295 - async: func.fn.node.async,
296 - generator: !!func.fn.node.generator,
297 - body: func.fn.node.body as t.BlockStatement,
298 - params: func.fn.node.params,
299 - },
300 - });
301 - continue;
302 - }
303 - for (const result of runPlayground(
304 - func.fn,
305 - {
306 - ...config,
307 - customHooks: new Map([...COMMON_HOOKS]),
308 - },
309 - getReactFunctionType(id),
310 - )) {
176 +
177 + transformOutput = invokeCompiler(
178 + source,
179 + language,
180 + {...config, customHooks: new Map([...COMMON_HOOKS])},
181 + result => {
182 switch (result.kind) {
183 case 'ast': {
313 - upsert({
314 - kind: 'ast',
315 - fnName,
316 - name: result.name,
317 - value: {
318 - type: 'FunctionDeclaration',
319 - id: withIdentifier(result.value.id),
320 - async: result.value.async,
321 - generator: result.value.generator,
322 - body: result.value.body,
323 - params: result.value.params,
324 - },
325 - });
184 break;
185 }
186 case 'hir': {
187 upsert({
188 kind: 'hir',
331 - fnName,
189 + fnName: result.value.id,
190 name: result.name,
191 value: printFunctionWithOutlined(result.value),
192 });
@@ -337,7 +195,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
195 case 'reactive': {
196 upsert({
197 kind: 'reactive',
340 - fnName,
198 + fnName: result.value.id,
199 name: result.name,
200 value: printReactiveFunctionWithOutlined(result.value),
201 });
@@ -346,7 +204,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
204 case 'debug': {
205 upsert({
206 kind: 'debug',
349 - fnName,
207 + fnName: null,
208 name: result.name,
209 value: result.value,
210 });
@@ -357,8 +215,8 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
215 throw new Error(`Unhandled result ${result}`);
216 }
217 }
360 - }
361 - }
218 + },
219 + );
220 } catch (err) {
221 /**
222 * error might be an invariant violation or other runtime error
@@ -385,7 +243,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
243 if (error.hasErrors()) {
244 return [{kind: 'err', results, error: error}, language];
245 }
388 - return [{kind: 'ok', results}, language];
246 + return [{kind: 'ok', results, transformOutput}, language];
247 }
248
249 export default function Editor(): JSX.Element {
@@ -405,7 +263,7 @@ export default function Editor(): JSX.Element {
263 } catch (e) {
264 invariant(e instanceof Error, 'Only Error may be caught.');
265 enqueueSnackbar(e.message, {
408 - variant: 'message',
266 + variant: 'warning',
267 ...createMessage(
268 'Bad URL - fell back to the default Playground.',
269 MessageLevel.Info,
compiler/apps/playground/components/Editor/Output.tsx
+22 -41
@@ -5,8 +5,6 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import generate from '@babel/generator';
9 -import * as t from '@babel/types';
8 import {
9 CodeIcon,
10 DocumentAddIcon,
@@ -21,17 +19,12 @@ import {memo, ReactNode, useEffect, useState} from 'react';
19 import {type Store} from '../../lib/stores';
20 import TabbedWindow from '../TabbedWindow';
21 import {monacoOptions} from './monacoOptions';
22 +import {BabelFileResult} from '@babel/core';
23 const MemoizedOutput = memo(Output);
24
25 export default MemoizedOutput;
26
27 export type PrintedCompilerPipelineValue =
29 - | {
30 - kind: 'ast';
31 - name: string;
32 - fnName: string | null;
33 - value: t.FunctionDeclaration;
34 - }
28 | {
29 kind: 'hir';
30 name: string;
@@ -41,8 +34,17 @@ export type PrintedCompilerPipelineValue =
34 | {kind: 'reactive'; name: string; fnName: string | null; value: string}
35 | {kind: 'debug'; name: string; fnName: string | null; value: string};
36
37 +export type CompilerTransformOutput = {
38 + code: string;
39 + sourceMaps: BabelFileResult['map'];
40 + language: 'flow' | 'typescript';
41 +};
42 export type CompilerOutput =
45 - | {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
43 + | {
44 + kind: 'ok';
45 + transformOutput: CompilerTransformOutput;
46 + results: Map<string, Array<PrintedCompilerPipelineValue>>;
47 + }
48 | {
49 kind: 'err';
50 results: Map<string, Array<PrintedCompilerPipelineValue>>;
@@ -61,7 +63,6 @@ async function tabify(
63 const tabs = new Map<string, React.ReactNode>();
64 const reorderedTabs = new Map<string, React.ReactNode>();
65 const concattedResults = new Map<string, string>();
64 - let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
66 // Concat all top level function declaration results into a single tab for each pass
67 for (const [passName, results] of compilerOutput.results) {
68 for (const result of results) {
@@ -87,9 +88,6 @@ async function tabify(
88 }
89 break;
90 }
90 - case 'ast':
91 - topLevelFnDecls.push(result.value);
92 - break;
91 case 'debug': {
92 concattedResults.set(passName, result.value);
93 break;
@@ -114,13 +112,17 @@ async function tabify(
112 lastPassOutput = text;
113 }
114 // Ensure that JS and the JS source map come first
117 - if (topLevelFnDecls.length > 0) {
118 - /**
119 - * Make a synthetic Program so we can have a single AST with all the top level
120 - * FunctionDeclarations
121 - */
122 - const ast = t.program(topLevelFnDecls);
123 - const {code, sourceMapUrl} = await codegen(ast, source);
115 + if (compilerOutput.kind === 'ok') {
116 + const {transformOutput} = compilerOutput;
117 + const sourceMapUrl = getSourceMapUrl(
118 + transformOutput.code,
119 + JSON.stringify(transformOutput.sourceMaps),
120 + );
121 + const code = await prettier.format(transformOutput.code, {
122 + semi: true,
123 + parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
124 + plugins: [parserBabel, prettierPluginEstree],
125 + });
126 reorderedTabs.set(
127 'JS',
128 <TextTabContent
@@ -147,27 +149,6 @@ async function tabify(
149 return reorderedTabs;
150 }
151
150 -async function codegen(
151 - ast: t.Program,
152 - source: string,
153 -): Promise<{code: any; sourceMapUrl: string | null}> {
154 - const generated = generate(
155 - ast,
156 - {sourceMaps: true, sourceFileName: 'input.js'},
157 - source,
158 - );
159 - const sourceMapUrl = getSourceMapUrl(
160 - generated.code,
161 - JSON.stringify(generated.map),
162 - );
163 - const codegenOutput = await prettier.format(generated.code, {
164 - semi: true,
165 - parser: 'babel',
166 - plugins: [parserBabel, prettierPluginEstree],
167 - });
168 - return {code: codegenOutput, sourceMapUrl};
169 -}
170 -
152 function utf16ToUTF8(s: string): string {
153 return unescape(encodeURIComponent(s));
154 }
compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts
+4 -1
@@ -39,7 +39,10 @@ export default function BabelPluginReactCompiler(
39 ) {
40 opts = injectReanimatedFlag(opts);
41 }
42 - if (isDev) {
42 + if (
43 + opts.environment.enableResetCacheOnSourceFileChanges !== false &&
44 + isDev
45 + ) {
46 opts = {
47 ...opts,
48 environment: {
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+2
@@ -15,6 +15,7 @@ import {
15 } from '../HIR/Environment';
16 import {hasOwnProperty} from '../Utils/utils';
17 import {fromZodError} from 'zod-validation-error';
18 +import {CompilerPipelineValue} from './Pipeline';
19
20 const PanicThresholdOptionsSchema = z.enum([
21 /*
@@ -209,6 +210,7 @@ export type LoggerEvent =
210
211 export type Logger = {
212 logEvent: (filename: string | null, event: LoggerEvent) => void;
213 + debugLogIRs?: (value: CompilerPipelineValue) => void;
214 };
215
216 export const defaultOptions: PluginOptions = {
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+67 -74
@@ -112,7 +112,7 @@ export type CompilerPipelineValue =
112 | {kind: 'reactive'; name: string; value: ReactiveFunction}
113 | {kind: 'debug'; name: string; value: string};
114
115 -export function* run(
115 +function run(
116 func: NodePath<
117 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
118 >,
@@ -122,7 +122,7 @@ export function* run(
122 logger: Logger | null,
123 filename: string | null,
124 code: string | null,
125 -): Generator<CompilerPipelineValue, CodegenFunction> {
125 +): CodegenFunction {
126 const contextIdentifiers = findContextIdentifiers(func);
127 const env = new Environment(
128 func.scope,
@@ -134,12 +134,17 @@ export function* run(
134 code,
135 useMemoCacheIdentifier,
136 );
137 - yield log({
137 + env.logger?.debugLogIRs?.({
138 + kind: 'debug',
139 + name: 'EnvironmentConfig',
140 + value: prettyFormat(env.config),
141 + });
142 + printLog({
143 kind: 'debug',
144 name: 'EnvironmentConfig',
145 value: prettyFormat(env.config),
146 });
142 - const ast = yield* runWithEnvironment(func, env);
147 + const ast = runWithEnvironment(func, env);
148 return ast;
149 }
150
@@ -147,17 +152,22 @@ export function* run(
152 * Note: this is split from run() to make `config` out of scope, so that all
153 * access to feature flags has to be through the Environment for consistency.
154 */
150 -function* runWithEnvironment(
155 +function runWithEnvironment(
156 func: NodePath<
157 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
158 >,
159 env: Environment,
155 -): Generator<CompilerPipelineValue, CodegenFunction> {
160 +): CodegenFunction {
161 + const log = (value: CompilerPipelineValue): CompilerPipelineValue => {
162 + printLog(value);
163 + env.logger?.debugLogIRs?.(value);
164 + return value;
165 + };
166 const hir = lower(func, env).unwrap();
157 - yield log({kind: 'hir', name: 'HIR', value: hir});
167 + log({kind: 'hir', name: 'HIR', value: hir});
168
169 pruneMaybeThrows(hir);
160 - yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
170 + log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
171
172 validateContextVariableLValues(hir);
173 validateUseMemo(hir);
@@ -168,35 +178,35 @@ function* runWithEnvironment(
178 !env.config.enableChangeDetectionForDebugging
179 ) {
180 dropManualMemoization(hir);
171 - yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
181 + log({kind: 'hir', name: 'DropManualMemoization', value: hir});
182 }
183
184 inlineImmediatelyInvokedFunctionExpressions(hir);
175 - yield log({
185 + log({
186 kind: 'hir',
187 name: 'InlineImmediatelyInvokedFunctionExpressions',
188 value: hir,
189 });
190
191 mergeConsecutiveBlocks(hir);
182 - yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
192 + log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
193
194 assertConsistentIdentifiers(hir);
195 assertTerminalSuccessorsExist(hir);
196
197 enterSSA(hir);
188 - yield log({kind: 'hir', name: 'SSA', value: hir});
198 + log({kind: 'hir', name: 'SSA', value: hir});
199
200 eliminateRedundantPhi(hir);
191 - yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
201 + log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
202
203 assertConsistentIdentifiers(hir);
204
205 constantPropagation(hir);
196 - yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
206 + log({kind: 'hir', name: 'ConstantPropagation', value: hir});
207
208 inferTypes(hir);
199 - yield log({kind: 'hir', name: 'InferTypes', value: hir});
209 + log({kind: 'hir', name: 'InferTypes', value: hir});
210
211 if (env.config.validateHooksUsage) {
212 validateHooksUsage(hir);
@@ -211,30 +221,30 @@ function* runWithEnvironment(
221 }
222
223 optimizePropsMethodCalls(hir);
214 - yield log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
224 + log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
225
226 analyseFunctions(hir);
217 - yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
227 + log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
228
229 inferReferenceEffects(hir);
220 - yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
230 + log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
231
232 validateLocalsNotReassignedAfterRender(hir);
233
234 // Note: Has to come after infer reference effects because "dead" code may still affect inference
235 deadCodeElimination(hir);
226 - yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
236 + log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
237
238 if (env.config.enableInstructionReordering) {
239 instructionReordering(hir);
230 - yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
240 + log({kind: 'hir', name: 'InstructionReordering', value: hir});
241 }
242
243 pruneMaybeThrows(hir);
234 - yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
244 + log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
245
246 inferMutableRanges(hir);
237 - yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
247 + log({kind: 'hir', name: 'InferMutableRanges', value: hir});
248
249 if (env.config.assertValidMutableRanges) {
250 assertValidMutableRanges(hir);
@@ -257,27 +267,27 @@ function* runWithEnvironment(
267 }
268
269 inferReactivePlaces(hir);
260 - yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
270 + log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
271
272 rewriteInstructionKindsBasedOnReassignment(hir);
263 - yield log({
273 + log({
274 kind: 'hir',
275 name: 'RewriteInstructionKindsBasedOnReassignment',
276 value: hir,
277 });
278
279 propagatePhiTypes(hir);
270 - yield log({
280 + log({
281 kind: 'hir',
282 name: 'PropagatePhiTypes',
283 value: hir,
284 });
285
286 inferReactiveScopeVariables(hir);
277 - yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
287 + log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
288
289 const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
280 - yield log({
290 + log({
291 kind: 'hir',
292 name: 'MemoizeFbtAndMacroOperandsInSameScope',
293 value: hir,
@@ -289,39 +299,39 @@ function* runWithEnvironment(
299
300 if (env.config.enableFunctionOutlining) {
301 outlineFunctions(hir, fbtOperands);
292 - yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
302 + log({kind: 'hir', name: 'OutlineFunctions', value: hir});
303 }
304
305 alignMethodCallScopes(hir);
296 - yield log({
306 + log({
307 kind: 'hir',
308 name: 'AlignMethodCallScopes',
309 value: hir,
310 });
311
312 alignObjectMethodScopes(hir);
303 - yield log({
313 + log({
314 kind: 'hir',
315 name: 'AlignObjectMethodScopes',
316 value: hir,
317 });
318
319 pruneUnusedLabelsHIR(hir);
310 - yield log({
320 + log({
321 kind: 'hir',
322 name: 'PruneUnusedLabelsHIR',
323 value: hir,
324 });
325
326 alignReactiveScopesToBlockScopesHIR(hir);
317 - yield log({
327 + log({
328 kind: 'hir',
329 name: 'AlignReactiveScopesToBlockScopesHIR',
330 value: hir,
331 });
332
333 mergeOverlappingReactiveScopesHIR(hir);
324 - yield log({
334 + log({
335 kind: 'hir',
336 name: 'MergeOverlappingReactiveScopesHIR',
337 value: hir,
@@ -329,7 +339,7 @@ function* runWithEnvironment(
339 assertValidBlockNesting(hir);
340
341 buildReactiveScopeTerminalsHIR(hir);
332 - yield log({
342 + log({
343 kind: 'hir',
344 name: 'BuildReactiveScopeTerminalsHIR',
345 value: hir,
@@ -338,14 +348,14 @@ function* runWithEnvironment(
348 assertValidBlockNesting(hir);
349
350 flattenReactiveLoopsHIR(hir);
341 - yield log({
351 + log({
352 kind: 'hir',
353 name: 'FlattenReactiveLoopsHIR',
354 value: hir,
355 });
356
357 flattenScopesWithHooksOrUseHIR(hir);
348 - yield log({
358 + log({
359 kind: 'hir',
360 name: 'FlattenScopesWithHooksOrUseHIR',
361 value: hir,
@@ -353,7 +363,7 @@ function* runWithEnvironment(
363 assertTerminalSuccessorsExist(hir);
364 assertTerminalPredsExist(hir);
365 propagateScopeDependenciesHIR(hir);
356 - yield log({
366 + log({
367 kind: 'hir',
368 name: 'PropagateScopeDependenciesHIR',
369 value: hir,
@@ -365,7 +375,7 @@ function* runWithEnvironment(
375
376 if (env.config.inlineJsxTransform) {
377 inlineJsxTransform(hir, env.config.inlineJsxTransform);
368 - yield log({
378 + log({
379 kind: 'hir',
380 name: 'inlineJsxTransform',
381 value: hir,
@@ -373,7 +383,7 @@ function* runWithEnvironment(
383 }
384
385 const reactiveFunction = buildReactiveFunction(hir);
376 - yield log({
386 + log({
387 kind: 'reactive',
388 name: 'BuildReactiveFunction',
389 value: reactiveFunction,
@@ -382,7 +392,7 @@ function* runWithEnvironment(
392 assertWellFormedBreakTargets(reactiveFunction);
393
394 pruneUnusedLabels(reactiveFunction);
385 - yield log({
395 + log({
396 kind: 'reactive',
397 name: 'PruneUnusedLabels',
398 value: reactiveFunction,
@@ -390,35 +400,35 @@ function* runWithEnvironment(
400 assertScopeInstructionsWithinScopes(reactiveFunction);
401
402 pruneNonEscapingScopes(reactiveFunction);
393 - yield log({
403 + log({
404 kind: 'reactive',
405 name: 'PruneNonEscapingScopes',
406 value: reactiveFunction,
407 });
408
409 pruneNonReactiveDependencies(reactiveFunction);
400 - yield log({
410 + log({
411 kind: 'reactive',
412 name: 'PruneNonReactiveDependencies',
413 value: reactiveFunction,
414 });
415
416 pruneUnusedScopes(reactiveFunction);
407 - yield log({
417 + log({
418 kind: 'reactive',
419 name: 'PruneUnusedScopes',
420 value: reactiveFunction,
421 });
422
423 mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
414 - yield log({
424 + log({
425 kind: 'reactive',
426 name: 'MergeReactiveScopesThatInvalidateTogether',
427 value: reactiveFunction,
428 });
429
430 pruneAlwaysInvalidatingScopes(reactiveFunction);
421 - yield log({
431 + log({
432 kind: 'reactive',
433 name: 'PruneAlwaysInvalidatingScopes',
434 value: reactiveFunction,
@@ -426,7 +436,7 @@ function* runWithEnvironment(
436
437 if (env.config.enableChangeDetectionForDebugging != null) {
438 pruneInitializationDependencies(reactiveFunction);
429 - yield log({
439 + log({
440 kind: 'reactive',
441 name: 'PruneInitializationDependencies',
442 value: reactiveFunction,
@@ -434,49 +444,49 @@ function* runWithEnvironment(
444 }
445
446 propagateEarlyReturns(reactiveFunction);
437 - yield log({
447 + log({
448 kind: 'reactive',
449 name: 'PropagateEarlyReturns',
450 value: reactiveFunction,
451 });
452
453 pruneUnusedLValues(reactiveFunction);
444 - yield log({
454 + log({
455 kind: 'reactive',
456 name: 'PruneUnusedLValues',
457 value: reactiveFunction,
458 });
459
460 promoteUsedTemporaries(reactiveFunction);
451 - yield log({
461 + log({
462 kind: 'reactive',
463 name: 'PromoteUsedTemporaries',
464 value: reactiveFunction,
465 });
466
467 extractScopeDeclarationsFromDestructuring(reactiveFunction);
458 - yield log({
468 + log({
469 kind: 'reactive',
470 name: 'ExtractScopeDeclarationsFromDestructuring',
471 value: reactiveFunction,
472 });
473
474 stabilizeBlockIds(reactiveFunction);
465 - yield log({
475 + log({
476 kind: 'reactive',
477 name: 'StabilizeBlockIds',
478 value: reactiveFunction,
479 });
480
481 const uniqueIdentifiers = renameVariables(reactiveFunction);
472 - yield log({
482 + log({
483 kind: 'reactive',
484 name: 'RenameVariables',
485 value: reactiveFunction,
486 });
487
488 pruneHoistedContexts(reactiveFunction);
479 - yield log({
489 + log({
490 kind: 'reactive',
491 name: 'PruneHoistedContexts',
492 value: reactiveFunction,
@@ -497,9 +507,9 @@ function* runWithEnvironment(
507 uniqueIdentifiers,
508 fbtOperands,
509 }).unwrap();
500 - yield log({kind: 'ast', name: 'Codegen', value: ast});
510 + log({kind: 'ast', name: 'Codegen', value: ast});
511 for (const outlined of ast.outlined) {
502 - yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
512 + log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
513 }
514
515 /**
@@ -525,7 +535,7 @@ export function compileFn(
535 filename: string | null,
536 code: string | null,
537 ): CodegenFunction {
528 - let generator = run(
538 + return run(
539 func,
540 config,
541 fnType,
@@ -534,15 +544,9 @@ export function compileFn(
544 filename,
545 code,
546 );
537 - while (true) {
538 - const next = generator.next();
539 - if (next.done) {
540 - return next.value;
541 - }
542 - }
547 }
548
545 -export function log(value: CompilerPipelineValue): CompilerPipelineValue {
549 +function printLog(value: CompilerPipelineValue): CompilerPipelineValue {
550 switch (value.kind) {
551 case 'ast': {
552 logCodegenFunction(value.name, value.value);
@@ -566,14 +570,3 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
570 }
571 return value;
572 }
569 -
570 -export function* runPlayground(
571 - func: NodePath<
572 - t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
573 - >,
574 - config: EnvironmentConfig,
575 - fnType: ReactFunctionType,
576 -): Generator<CompilerPipelineValue, CodegenFunction> {
577 - const ast = yield* run(func, config, fnType, '_c', null, null, null);
578 - return ast;
579 -}
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+23 -6
@@ -168,11 +168,19 @@ const EnvironmentConfigSchema = z.object({
168 customMacros: z.nullable(z.array(MacroSchema)).default(null),
169
170 /**
171 - * Enable a check that resets the memoization cache when the source code of the file changes.
172 - * This is intended to support hot module reloading (HMR), where the same runtime component
173 - * instance will be reused across different versions of the component source.
171 + * Enable a check that resets the memoization cache when the source code of
172 + * the file changes. This is intended to support hot module reloading (HMR),
173 + * where the same runtime component instance will be reused across different
174 + * versions of the component source.
175 + *
176 + * When set to
177 + * - true: code for HMR support is always generated, regardless of NODE_ENV
178 + * or `globalThis.__DEV__`
179 + * - false: code for HMR support is not generated
180 + * - null: (default) code for HMR support is conditionally generated dependent
181 + * on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
182 */
175 - enableResetCacheOnSourceFileChanges: z.boolean().default(false),
183 + enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
184
185 /**
186 * Enable using information from existing useMemo/useCallback to understand when a value is done
@@ -708,7 +716,10 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
716 continue;
717 }
718
711 - if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
719 + if (
720 + key !== 'enableResetCacheOnSourceFileChanges' &&
721 + typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
722 + ) {
723 // skip parsing non-boolean properties
724 continue;
725 }
@@ -718,9 +729,15 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
729 maybeConfig[key] = false;
730 }
731 }
721 -
732 const config = EnvironmentConfigSchema.safeParse(maybeConfig);
733 if (config.success) {
734 + /**
735 + * Unless explicitly enabled, do not insert HMR handling code
736 + * in test fixtures or playground to reduce visual noise.
737 + */
738 + if (config.data.enableResetCacheOnSourceFileChanges == null) {
739 + config.data.enableResetCacheOnSourceFileChanges = false;
740 + }
741 return config.data;
742 }
743 CompilerError.invariant(false, {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-module-scope-usememo-function-scope.expect.md new
+29
@@ -0,0 +1,29 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @compilationMode(all)
6 +'use no memo';
7 +
8 +function TestComponent({x}) {
9 + 'use memo';
10 + return <Button>{x}</Button>;
11 +}
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +// @compilationMode(all)
19 +"use no memo";
20 +
21 +function TestComponent({ x }) {
22 + "use memo";
23 + return <Button>{x}</Button>;
24 +}
25 +
26 +```
27 +
28 +### Eval output
29 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-module-scope-usememo-function-scope.js new
+7
@@ -0,0 +1,7 @@
1 +// @compilationMode(all)
2 +'use no memo';
3 +
4 +function TestComponent({x}) {
5 + 'use memo';
6 + return <Button>{x}</Button>;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts
+1
@@ -25,6 +25,7 @@ describe('parseConfigPragmaForTests()', () => {
25 enableUseTypeAnnotations: true,
26 validateNoSetStateInPassiveEffects: true,
27 validateNoSetStateInRender: false,
28 + enableResetCacheOnSourceFileChanges: false,
29 });
30 });
31 });
compiler/packages/babel-plugin-react-compiler/src/index.ts
-2
@@ -17,8 +17,6 @@ export {
17 compileFn as compile,
18 compileProgram,
19 parsePluginOptions,
20 - run,
21 - runPlayground,
20 OPT_OUT_DIRECTIVES,
21 OPT_IN_DIRECTIVES,
22 findDirectiveEnablingMemoization,