@samitouri / QOS-React / commits / 1a27af3607

[playground] Update the playground UI (#34468)

<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary Updated the UI of the React compiler playground. The config, Input, and Output panels will now span the viewport width when "Show Internals" is not toggled on. When "Show Internals" is toggled on, the old vertical accordion tabs are still used. Going to add support for the "Applied Configs" tabs underneath the "Config Overrides" tab next. <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> ## How did you test this change? https://github.com/user-attachments/assets/b8eab028-f58c-4cb9-a8b2-0f098f2cc262 <!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. How exactly did you verify that your PR solves the issue you wanted to solve? If you leave this empty, your PR will very likely be closed. -->

Eugene Choi committed Sep 12, 2025 at 11:43 UTC 1a27af36073d7dffcc7a4284ed569af6f804747a
10 files changed +495 -292
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt
+1 -2
@@ -1,5 +1,4 @@
1 -import { c as _c } from "react/compiler-runtime"; // 
2 -@compilationMode:"all"
1 +import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
2 function nonReactFn() {
3   const $ = _c(1);
4   let t0;
compiler/apps/playground/components/AccordionWindow.tsx new
+106
@@ -0,0 +1,106 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {Resizable} from 're-resizable';
9 +import React, {useCallback} from 'react';
10 +
11 +type TabsRecord = Map<string, React.ReactNode>;
12 +
13 +export default function AccordionWindow(props: {
14 + defaultTab: string | null;
15 + tabs: TabsRecord;
16 + tabsOpen: Set<string>;
17 + setTabsOpen: (newTab: Set<string>) => void;
18 + changedPasses: Set<string>;
19 +}): React.ReactElement {
20 + if (props.tabs.size === 0) {
21 + return (
22 + <div
23 + className="flex items-center justify-center"
24 + style={{width: 'calc(100vw - 650px)'}}>
25 + No compiler output detected, see errors below
26 + </div>
27 + );
28 + }
29 + return (
30 + <div className="flex flex-row h-full">
31 + {Array.from(props.tabs.keys()).map(name => {
32 + return (
33 + <AccordionWindowItem
34 + name={name}
35 + key={name}
36 + tabs={props.tabs}
37 + tabsOpen={props.tabsOpen}
38 + setTabsOpen={props.setTabsOpen}
39 + hasChanged={props.changedPasses.has(name)}
40 + />
41 + );
42 + })}
43 + </div>
44 + );
45 +}
46 +
47 +function AccordionWindowItem({
48 + name,
49 + tabs,
50 + tabsOpen,
51 + setTabsOpen,
52 + hasChanged,
53 +}: {
54 + name: string;
55 + tabs: TabsRecord;
56 + tabsOpen: Set<string>;
57 + setTabsOpen: (newTab: Set<string>) => void;
58 + hasChanged: boolean;
59 +}): React.ReactElement {
60 + const isShow = tabsOpen.has(name);
61 +
62 + const toggleTabs = useCallback(() => {
63 + const nextState = new Set(tabsOpen);
64 + if (nextState.has(name)) {
65 + nextState.delete(name);
66 + } else {
67 + nextState.add(name);
68 + }
69 + setTabsOpen(nextState);
70 + }, [tabsOpen, name, setTabsOpen]);
71 +
72 + // Replace spaces with non-breaking spaces
73 + const displayName = name.replace(/ /g, '\u00A0');
74 +
75 + return (
76 + <div key={name} className="flex flex-row">
77 + {isShow ? (
78 + <Resizable className="border-r" minWidth={550} enable={{right: true}}>
79 + <h2
80 + title="Minimize tab"
81 + aria-label="Minimize tab"
82 + onClick={toggleTabs}
83 + className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
84 + hasChanged ? 'font-bold' : 'font-light'
85 + } text-secondary hover:text-link`}>
86 + - {displayName}
87 + </h2>
88 + {tabs.get(name) ?? <div>No output for {name}</div>}
89 + </Resizable>
90 + ) : (
91 + <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
92 + <button
93 + title={`Expand compiler tab: ${name}`}
94 + aria-label={`Expand compiler tab: ${name}`}
95 + style={{transform: 'rotate(90deg) translate(-50%)'}}
96 + onClick={toggleTabs}
97 + className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
98 + hasChanged ? 'font-bold' : 'font-light'
99 + } text-secondary hover:text-link`}>
100 + {displayName}
101 + </button>
102 + </div>
103 + )}
104 + </div>
105 + );
106 +}
compiler/apps/playground/components/Editor/ConfigEditor.tsx
+92 -53
@@ -8,10 +8,11 @@
8 import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9 import type {editor} from 'monaco-editor';
10 import * as monaco from 'monaco-editor';
11 -import React, {useState, useCallback} from 'react';
11 +import React, {useState} from 'react';
12 import {Resizable} from 're-resizable';
13 import {useStore, useStoreDispatch} from '../StoreContext';
14 import {monacoOptions} from './monacoOptions';
15 +import {IconChevron} from '../Icons/IconChevron';
16
17 // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
18 import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
@@ -20,13 +21,26 @@ loader.config({monaco});
21
22 export default function ConfigEditor(): React.ReactElement {
23 const [isExpanded, setIsExpanded] = useState(false);
24 +
25 + return (
26 + <div className="flex flex-row relative">
27 + {isExpanded ? (
28 + <ExpandedEditor onToggle={setIsExpanded} />
29 + ) : (
30 + <CollapsedEditor onToggle={setIsExpanded} />
31 + )}
32 + </div>
33 + );
34 +}
35 +
36 +function ExpandedEditor({
37 + onToggle,
38 +}: {
39 + onToggle: (expanded: boolean) => void;
40 +}): React.ReactElement {
41 const store = useStore();
42 const dispatchStore = useStoreDispatch();
43
26 - const toggleExpanded = useCallback(() => {
27 - setIsExpanded(prev => !prev);
28 - }, []);
29 -
44 const handleChange: (value: string | undefined) => void = value => {
45 if (value === undefined) return;
46
@@ -68,57 +82,82 @@ export default function ConfigEditor(): React.ReactElement {
82 };
83
84 return (
71 - <div className="flex flex-row relative">
72 - {isExpanded ? (
73 - <Resizable
74 - className="border-r"
75 - minWidth={300}
76 - maxWidth={600}
77 - defaultSize={{width: 350, height: 'auto'}}
78 - enable={{right: true}}>
79 - <h2
80 - title="Minimize config editor"
81 - aria-label="Minimize config editor"
82 - onClick={toggleExpanded}
83 - className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
84 - - Config Overrides
85 + <Resizable
86 + className="border-r"
87 + minWidth={300}
88 + maxWidth={600}
89 + defaultSize={{width: 350, height: 'auto'}}
90 + enable={{right: true, bottom: false}}
91 + style={{position: 'relative', height: 'calc(100vh - 3.5rem)'}}>
92 + <div className="bg-gray-700 p-2">
93 + <div className="pb-2">
94 + <h2 className="inline-block text-secondary-dark text-center outline-none py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full capitalize whitespace-nowrap text-sm">
95 + Config Overrides
96 </h2>
86 - <div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
87 - <MonacoEditor
88 - path={'config.ts'}
89 - language={'typescript'}
90 - value={store.config}
91 - onMount={handleMount}
92 - onChange={handleChange}
93 - options={{
94 - ...monacoOptions,
95 - lineNumbers: 'off',
96 - folding: false,
97 - renderLineHighlight: 'none',
98 - scrollBeyondLastLine: false,
99 - hideCursorInOverviewRuler: true,
100 - overviewRulerBorder: false,
101 - overviewRulerLanes: 0,
102 - fontSize: 12,
103 - }}
104 - />
105 - </div>
106 - </Resizable>
107 - ) : (
108 - <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
109 - <button
110 - title="Expand config editor"
111 - aria-label="Expand config editor"
112 - style={{
113 - transform: 'rotate(90deg) translate(-50%)',
114 - whiteSpace: 'nowrap',
97 + </div>
98 + <div
99 + className="absolute w-10 h-16 bg-gray-700 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[5] cursor-pointer"
100 + title="Minimize config editor"
101 + onClick={() => onToggle(false)}
102 + style={{
103 + top: '50%',
104 + marginTop: '-32px',
105 + right: '-32px',
106 + borderTopLeftRadius: 0,
107 + borderBottomLeftRadius: 0,
108 + }}>
109 + <IconChevron
110 + displayDirection="left"
111 + className="text-secondary-dark"
112 + />
113 + </div>
114 + <div className="h-[calc(100vh_-_3.5rem_-_3.5rem)] rounded-lg overflow-hidden">
115 + <MonacoEditor
116 + path={'config.ts'}
117 + language={'typescript'}
118 + value={store.config}
119 + onMount={handleMount}
120 + onChange={handleChange}
121 + options={{
122 + ...monacoOptions,
123 + lineNumbers: 'off',
124 + folding: false,
125 + renderLineHighlight: 'none',
126 + hideCursorInOverviewRuler: true,
127 + overviewRulerBorder: false,
128 + overviewRulerLanes: 0,
129 + fontSize: 12,
130 + scrollBeyondLastLine: false,
131 }}
116 - onClick={toggleExpanded}
117 - className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
118 - Config Overrides
119 - </button>
132 + />
133 </div>
121 - )}
134 + </div>
135 + </Resizable>
136 + );
137 +}
138 +
139 +function CollapsedEditor({
140 + onToggle,
141 +}: {
142 + onToggle: (expanded: boolean) => void;
143 +}): React.ReactElement {
144 + return (
145 + <div
146 + className="w-4"
147 + style={{height: 'calc(100vh - 3.5rem)', position: 'relative'}}>
148 + <div
149 + className="absolute w-10 h-16 bg-gray-700 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[5] cursor-pointer"
150 + title="Expand config editor"
151 + onClick={() => onToggle(true)}
152 + style={{
153 + top: '50%',
154 + marginTop: '-32px',
155 + left: '-8px',
156 + borderTopLeftRadius: 0,
157 + borderBottomLeftRadius: 0,
158 + }}>
159 + <IconChevron displayDirection="right" className="text-secondary-dark" />
160 + </div>
161 </div>
162 );
163 }
compiler/apps/playground/components/Editor/EditorImpl.tsx
+147 -116
@@ -24,7 +24,6 @@ import BabelPluginReactCompiler, {
24 printFunctionWithOutlined,
25 type LoggerEvent,
26 } from 'babel-plugin-react-compiler';
27 -import clsx from 'clsx';
27 import invariant from 'invariant';
28 import {useSnackbar} from 'notistack';
29 import {useDeferredValue, useMemo} from 'react';
@@ -47,7 +46,6 @@ import {
46 PrintedCompilerPipelineValue,
47 } from './Output';
48 import {transformFromAstSync} from '@babel/core';
50 -import {useSearchParams} from 'next/navigation';
49
50 function parseInput(
51 input: string,
@@ -144,6 +142,61 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
142 ],
143 ];
144
145 +function parseOptions(
146 + source: string,
147 + mode: 'compiler' | 'linter',
148 + configOverrides: string,
149 +): PluginOptions {
150 + // Extract the first line to quickly check for custom test directives
151 + const pragma = source.substring(0, source.indexOf('\n'));
152 +
153 + const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
154 + compilationMode: 'infer',
155 + environment:
156 + mode === 'linter'
157 + ? {
158 + // enabled in compiler
159 + validateRefAccessDuringRender: false,
160 + // enabled in linter
161 + validateNoSetStateInRender: true,
162 + validateNoSetStateInEffects: true,
163 + validateNoJSXInTryStatements: true,
164 + validateNoImpureFunctionsInRender: true,
165 + validateStaticComponents: true,
166 + validateNoFreezingKnownMutableFunctions: true,
167 + validateNoVoidUseMemo: true,
168 + }
169 + : {
170 + /* use defaults for compiler mode */
171 + },
172 + });
173 +
174 + // Parse config overrides from config editor
175 + let configOverrideOptions: any = {};
176 + const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
177 + // TODO: initialize store with URL params, not empty store
178 + if (configOverrides.trim()) {
179 + if (configMatch && configMatch[1]) {
180 + const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
181 + configOverrideOptions = new Function(`return (${configString})`)();
182 + } else {
183 + throw new Error('Invalid override format');
184 + }
185 + }
186 +
187 + const opts: PluginOptions = parsePluginOptions({
188 + ...parsedPragmaOptions,
189 + ...configOverrideOptions,
190 + environment: {
191 + ...parsedPragmaOptions.environment,
192 + ...configOverrideOptions.environment,
193 + customHooks: new Map([...COMMON_HOOKS]),
194 + },
195 + });
196 +
197 + return opts;
198 +}
199 +
200 function compile(
201 source: string,
202 mode: 'compiler' | 'linter',
@@ -167,120 +220,94 @@ function compile(
220 language = 'typescript';
221 }
222 let transformOutput;
223 +
224 + let baseOpts: PluginOptions | null = null;
225 try {
171 - // Extract the first line to quickly check for custom test directives
172 - const pragma = source.substring(0, source.indexOf('\n'));
173 - const logIR = (result: CompilerPipelineValue): void => {
174 - switch (result.kind) {
175 - case 'ast': {
176 - break;
177 - }
178 - case 'hir': {
179 - upsert({
180 - kind: 'hir',
181 - fnName: result.value.id,
182 - name: result.name,
183 - value: printFunctionWithOutlined(result.value),
184 - });
185 - break;
186 - }
187 - case 'reactive': {
188 - upsert({
189 - kind: 'reactive',
190 - fnName: result.value.id,
191 - name: result.name,
192 - value: printReactiveFunctionWithOutlined(result.value),
193 - });
194 - break;
195 - }
196 - case 'debug': {
197 - upsert({
198 - kind: 'debug',
199 - fnName: null,
200 - name: result.name,
201 - value: result.value,
202 - });
203 - break;
204 - }
205 - default: {
206 - const _: never = result;
207 - throw new Error(`Unhandled result ${result}`);
226 + baseOpts = parseOptions(source, mode, configOverrides);
227 + } catch (err) {
228 + error.details.push(
229 + new CompilerErrorDetail({
230 + category: ErrorCategory.Config,
231 + reason: `Unexpected failure when transforming configs! \n${err}`,
232 + loc: null,
233 + suggestions: null,
234 + }),
235 + );
236 + }
237 + if (baseOpts) {
238 + try {
239 + const logIR = (result: CompilerPipelineValue): void => {
240 + switch (result.kind) {
241 + case 'ast': {
242 + break;
243 + }
244 + case 'hir': {
245 + upsert({
246 + kind: 'hir',
247 + fnName: result.value.id,
248 + name: result.name,
249 + value: printFunctionWithOutlined(result.value),
250 + });
251 + break;
252 + }
253 + case 'reactive': {
254 + upsert({
255 + kind: 'reactive',
256 + fnName: result.value.id,
257 + name: result.name,
258 + value: printReactiveFunctionWithOutlined(result.value),
259 + });
260 + break;
261 + }
262 + case 'debug': {
263 + upsert({
264 + kind: 'debug',
265 + fnName: null,
266 + name: result.name,
267 + value: result.value,
268 + });
269 + break;
270 + }
271 + default: {
272 + const _: never = result;
273 + throw new Error(`Unhandled result ${result}`);
274 + }
275 }
209 - }
210 - };
211 - const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
212 - compilationMode: 'infer',
213 - environment:
214 - mode === 'linter'
215 - ? {
216 - // enabled in compiler
217 - validateRefAccessDuringRender: false,
218 - // enabled in linter
219 - validateNoSetStateInRender: true,
220 - validateNoSetStateInEffects: true,
221 - validateNoJSXInTryStatements: true,
222 - validateNoImpureFunctionsInRender: true,
223 - validateStaticComponents: true,
224 - validateNoFreezingKnownMutableFunctions: true,
225 - validateNoVoidUseMemo: true,
276 + };
277 + // Add logger options to the parsed options
278 + const opts = {
279 + ...baseOpts,
280 + logger: {
281 + debugLogIRs: logIR,
282 + logEvent: (_filename: string | null, event: LoggerEvent) => {
283 + if (event.kind === 'CompileError') {
284 + otherErrors.push(event.detail);
285 }
227 - : {
228 - /* use defaults for compiler mode */
229 - },
230 - });
231 -
232 - // Parse config overrides from config editor
233 - let configOverrideOptions: any = {};
234 - const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
235 - // TODO: initialize store with URL params, not empty store
236 - if (configOverrides.trim()) {
237 - if (configMatch && configMatch[1]) {
238 - const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
239 - configOverrideOptions = new Function(`return (${configString})`)();
240 - } else {
241 - throw new Error('Invalid config overrides');
242 - }
243 - }
244 -
245 - const opts: PluginOptions = parsePluginOptions({
246 - ...parsedPragmaOptions,
247 - ...configOverrideOptions,
248 - environment: {
249 - ...parsedPragmaOptions.environment,
250 - ...configOverrideOptions.environment,
251 - customHooks: new Map([...COMMON_HOOKS]),
252 - },
253 - logger: {
254 - debugLogIRs: logIR,
255 - logEvent: (_filename: string | null, event: LoggerEvent) => {
256 - if (event.kind === 'CompileError') {
257 - otherErrors.push(event.detail);
258 - }
286 + },
287 },
260 - },
261 - });
262 - transformOutput = invokeCompiler(source, language, opts);
263 - } catch (err) {
264 - /**
265 - * error might be an invariant violation or other runtime error
266 - * (i.e. object shape that is not CompilerError)
267 - */
268 - if (err instanceof CompilerError && err.details.length > 0) {
269 - error.merge(err);
270 - } else {
288 + };
289 + transformOutput = invokeCompiler(source, language, opts);
290 + } catch (err) {
291 /**
272 - * Handle unexpected failures by logging (to get a stack trace)
273 - * and reporting
292 + * error might be an invariant violation or other runtime error
293 + * (i.e. object shape that is not CompilerError)
294 */
275 - console.error(err);
276 - error.details.push(
277 - new CompilerErrorDetail({
278 - category: ErrorCategory.Invariant,
279 - reason: `Unexpected failure when transforming input! ${err}`,
280 - loc: null,
281 - suggestions: null,
282 - }),
283 - );
295 + if (err instanceof CompilerError && err.details.length > 0) {
296 + error.merge(err);
297 + } else {
298 + /**
299 + * Handle unexpected failures by logging (to get a stack trace)
300 + * and reporting
301 + */
302 + error.details.push(
303 + new CompilerErrorDetail({
304 + category: ErrorCategory.Invariant,
305 + reason: `Unexpected failure when transforming input! \n${err}`,
306 + loc: null,
307 + suggestions: null,
308 + }),
309 + );
310 + }
311 }
312 }
313 // Only include logger errors if there weren't other errors
@@ -350,13 +377,17 @@ export default function Editor(): JSX.Element {
377 }
378 return (
379 <>
353 - <div className="relative flex basis top-14">
354 - <ConfigEditor />
355 - <div className={clsx('relative sm:basis-1/4')}>
356 - <Input language={language} errors={errors} />
380 + <div className="relative flex top-14">
381 + <div className="flex-shrink-0">
382 + <ConfigEditor />
383 </div>
358 - <div className={clsx('flex sm:flex flex-wrap')}>
359 - <Output store={deferredStore} compilerOutput={mergedOutput} />
384 + <div className="flex flex-1 min-w-0">
385 + <div className="flex-1 min-w-[550px] sm:min-w-0">
386 + <Input language={language} errors={errors} />
387 + </div>
388 + <div className="flex-1 min-w-[550px] sm:min-w-0">
389 + <Output store={deferredStore} compilerOutput={mergedOutput} />
390 + </div>
391 </div>
392 </div>
393 </>
compiler/apps/playground/components/Editor/Input.tsx
+47 -22
@@ -6,7 +6,10 @@
6 */
7
8 import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9 -import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
9 +import {
10 + CompilerErrorDetail,
11 + CompilerDiagnostic,
12 +} from 'babel-plugin-react-compiler';
13 import invariant from 'invariant';
14 import type {editor} from 'monaco-editor';
15 import * as monaco from 'monaco-editor';
@@ -14,6 +17,7 @@ import {Resizable} from 're-resizable';
17 import {useEffect, useState} from 'react';
18 import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
19 import {useStore, useStoreDispatch} from '../StoreContext';
20 +import TabbedWindow from '../TabbedWindow';
21 import {monacoOptions} from './monacoOptions';
22 // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
23 import React$Types from '../../node_modules/@types/react/index.d.ts';
@@ -21,7 +25,7 @@ import React$Types from '../../node_modules/@types/react/index.d.ts';
25 loader.config({monaco});
26
27 type Props = {
24 - errors: Array<CompilerErrorDetail>;
28 + errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
29 language: 'flow' | 'typescript';
30 };
31
@@ -135,30 +139,51 @@ export default function Input({errors, language}: Props): JSX.Element {
139 });
140 };
141
142 + const editorContent = (
143 + <MonacoEditor
144 + path={'index.js'}
145 + /**
146 + * .js and .jsx files are specified to be TS so that Monaco can actually
147 + * check their syntax using its TS language service. They are still JS files
148 + * due to their extensions, so TS language features don't work.
149 + */
150 + language={'javascript'}
151 + value={store.source}
152 + onMount={handleMount}
153 + onChange={handleChange}
154 + options={monacoOptions}
155 + />
156 + );
157 +
158 + const tabs = new Map([['Input', editorContent]]);
159 + const [activeTab, setActiveTab] = useState('Input');
160 +
161 + const tabbedContent = (
162 + <div className="flex flex-col h-full">
163 + <TabbedWindow
164 + tabs={tabs}
165 + activeTab={activeTab}
166 + onTabChange={setActiveTab}
167 + />
168 + </div>
169 + );
170 +
171 return (
172 <div className="relative flex flex-col flex-none border-r border-gray-200">
140 - <Resizable
141 - minWidth={650}
142 - enable={{right: true}}
143 - /**
144 - * Restrict MonacoEditor's height, since the config autoLayout:true
145 - * will grow the editor to fit within parent element
146 - */
147 - className="!h-[calc(100vh_-_3.5rem)]">
148 - <MonacoEditor
149 - path={'index.js'}
173 + {store.showInternals ? (
174 + <Resizable
175 + minWidth={550}
176 + enable={{right: true}}
177 /**
151 - * .js and .jsx files are specified to be TS so that Monaco can actually
152 - * check their syntax using its TS language service. They are still JS files
153 - * due to their extensions, so TS language features don't work.
178 + * Restrict MonacoEditor's height, since the config autoLayout:true
179 + * will grow the editor to fit within parent element
180 */
155 - language={'javascript'}
156 - value={store.source}
157 - onMount={handleMount}
158 - onChange={handleChange}
159 - options={monacoOptions}
160 - />
161 - </Resizable>
181 + className="!h-[calc(100vh_-_3.5rem)]">
182 + {tabbedContent}
183 + </Resizable>
184 + ) : (
185 + <div className="!h-[calc(100vh_-_3.5rem)]">{tabbedContent}</div>
186 + )}
187 </div>
188 );
189 }
compiler/apps/playground/components/Editor/Output.tsx
+22 -8
@@ -21,13 +21,17 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
21 import * as prettier from 'prettier/standalone';
22 import {memo, ReactNode, useEffect, useState} from 'react';
23 import {type Store} from '../../lib/stores';
24 +import AccordionWindow from '../AccordionWindow';
25 import TabbedWindow from '../TabbedWindow';
26 import {monacoOptions} from './monacoOptions';
27 import {BabelFileResult} from '@babel/core';
28 +
29 const MemoizedOutput = memo(Output);
30
31 export default MemoizedOutput;
32
33 +export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
34 +
35 export type PrintedCompilerPipelineValue =
36 | {
37 kind: 'hir';
@@ -71,7 +75,7 @@ async function tabify(
75 const concattedResults = new Map<string, string>();
76 // Concat all top level function declaration results into a single tab for each pass
77 for (const [passName, results] of compilerOutput.results) {
74 - if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') {
78 + if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
79 continue;
80 }
81 for (const result of results) {
@@ -215,6 +219,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
219 const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
220 () => new Map(),
221 );
222 + const [activeTab, setActiveTab] = useState<string>('Output');
223
224 /*
225 * Update the active tab back to the output or errors tab when the compilation state
@@ -226,6 +231,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
231 if (compilerOutput.kind !== previousOutputKind) {
232 setPreviousOutputKind(compilerOutput.kind);
233 setTabsOpen(new Set(['Output']));
234 + setActiveTab('Output');
235 }
236
237 useEffect(() => {
@@ -249,16 +255,24 @@ function Output({store, compilerOutput}: Props): JSX.Element {
255 }
256 }
257
252 - return (
253 - <>
258 + if (!store.showInternals) {
259 + return (
260 <TabbedWindow
255 - defaultTab={store.showInternals ? 'HIR' : 'Output'}
256 - setTabsOpen={setTabsOpen}
257 - tabsOpen={tabsOpen}
261 tabs={tabs}
259 - changedPasses={changedPasses}
262 + activeTab={activeTab}
263 + onTabChange={setActiveTab}
264 />
261 - </>
265 + );
266 + }
267 +
268 + return (
269 + <AccordionWindow
270 + defaultTab={store.showInternals ? 'HIR' : 'Output'}
271 + setTabsOpen={setTabsOpen}
272 + tabsOpen={tabsOpen}
273 + tabs={tabs}
274 + changedPasses={changedPasses}
275 + />
276 );
277 }
278
compiler/apps/playground/components/Header.tsx
+1 -1
@@ -72,7 +72,7 @@ export default function Header(): JSX.Element {
72 'before:bg-white before:rounded-full before:transition-transform before:duration-250',
73 'focus-within:shadow-[0_0_1px_#2196F3]',
74 store.showInternals
75 - ? 'bg-blue-500 before:translate-x-3.5'
75 + ? 'bg-link before:translate-x-3.5'
76 : 'bg-gray-300',
77 )}></span>
78 </label>
compiler/apps/playground/components/Icons/IconChevron.tsx new
+41
@@ -0,0 +1,41 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {memo} from 'react';
9 +
10 +export const IconChevron = memo<
11 + JSX.IntrinsicElements['svg'] & {
12 + /**
13 + * The direction the arrow should point.
14 + */
15 + displayDirection: 'right' | 'left';
16 + }
17 +>(function IconChevron({className, displayDirection, ...props}) {
18 + const rotationClass =
19 + displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
20 + const classes = className ? `${rotationClass} ${className}` : rotationClass;
21 +
22 + return (
23 + <svg
24 + className={classes}
25 + xmlns="http://www.w3.org/2000/svg"
26 + width="20"
27 + height="20"
28 + viewBox="0 0 20 20"
29 + {...props}>
30 + <g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
31 + <path
32 + fill="currentColor"
33 + fillRule="nonzero"
34 + d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
35 + transform="translate(356.5 164.5)"
36 + />
37 + <polygon points="446 418 466 418 466 398 446 398" />
38 + </g>
39 + </svg>
40 + );
41 +});
compiler/apps/playground/components/TabbedWindow.tsx
+33 -89
@@ -4,103 +4,47 @@
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7 +import React from 'react';
8 +import clsx from 'clsx';
9
8 -import {Resizable} from 're-resizable';
9 -import React, {useCallback} from 'react';
10 -
11 -type TabsRecord = Map<string, React.ReactNode>;
12 -
13 -export default function TabbedWindow(props: {
14 - defaultTab: string | null;
15 - tabs: TabsRecord;
16 - tabsOpen: Set<string>;
17 - setTabsOpen: (newTab: Set<string>) => void;
18 - changedPasses: Set<string>;
10 +export default function TabbedWindow({
11 + tabs,
12 + activeTab,
13 + onTabChange,
14 +}: {
15 + tabs: Map<string, React.ReactNode>;
16 + activeTab: string;
17 + onTabChange: (tab: string) => void;
18 }): React.ReactElement {
20 - if (props.tabs.size === 0) {
19 + if (tabs.size === 0) {
20 return (
22 - <div
23 - className="flex items-center justify-center"
24 - style={{width: 'calc(100vw - 650px)'}}>
21 + <div className="flex items-center justify-center flex-1 max-w-full">
22 No compiler output detected, see errors below
23 </div>
24 );
25 }
26 return (
30 - <div className="flex flex-row">
31 - {Array.from(props.tabs.keys()).map(name => {
32 - return (
33 - <TabbedWindowItem
34 - name={name}
35 - key={name}
36 - tabs={props.tabs}
37 - tabsOpen={props.tabsOpen}
38 - setTabsOpen={props.setTabsOpen}
39 - hasChanged={props.changedPasses.has(name)}
40 - />
41 - );
42 - })}
43 - </div>
44 - );
45 -}
46 -
47 -function TabbedWindowItem({
48 - name,
49 - tabs,
50 - tabsOpen,
51 - setTabsOpen,
52 - hasChanged,
53 -}: {
54 - name: string;
55 - tabs: TabsRecord;
56 - tabsOpen: Set<string>;
57 - setTabsOpen: (newTab: Set<string>) => void;
58 - hasChanged: boolean;
59 -}): React.ReactElement {
60 - const isShow = tabsOpen.has(name);
61 -
62 - const toggleTabs = useCallback(() => {
63 - const nextState = new Set(tabsOpen);
64 - if (nextState.has(name)) {
65 - nextState.delete(name);
66 - } else {
67 - nextState.add(name);
68 - }
69 - setTabsOpen(nextState);
70 - }, [tabsOpen, name, setTabsOpen]);
71 -
72 - // Replace spaces with non-breaking spaces
73 - const displayName = name.replace(/ /g, '\u00A0');
74 -
75 - return (
76 - <div key={name} className="flex flex-row">
77 - {isShow ? (
78 - <Resizable className="border-r" minWidth={550} enable={{right: true}}>
79 - <h2
80 - title="Minimize tab"
81 - aria-label="Minimize tab"
82 - onClick={toggleTabs}
83 - className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
84 - hasChanged ? 'font-bold' : 'font-light'
85 - } text-secondary hover:text-link`}>
86 - - {displayName}
87 - </h2>
88 - {tabs.get(name) ?? <div>No output for {name}</div>}
89 - </Resizable>
90 - ) : (
91 - <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
92 - <button
93 - title={`Expand compiler tab: ${name}`}
94 - aria-label={`Expand compiler tab: ${name}`}
95 - style={{transform: 'rotate(90deg) translate(-50%)'}}
96 - onClick={toggleTabs}
97 - className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
98 - hasChanged ? 'font-bold' : 'font-light'
99 - } text-secondary hover:text-link`}>
100 - {displayName}
101 - </button>
102 - </div>
103 - )}
27 + <div className="flex flex-col h-full max-w-full">
28 + <div className="flex p-2 flex-shrink-0">
29 + {Array.from(tabs.keys()).map(tab => {
30 + const isActive = activeTab === tab;
31 + return (
32 + <button
33 + key={tab}
34 + onClick={() => onTabChange(tab)}
35 + className={clsx(
36 + 'active:scale-95 transition-transform text-center outline-none py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full capitalize whitespace-nowrap text-sm',
37 + !isActive && 'hover:bg-primary/5',
38 + isActive && 'bg-highlight text-link',
39 + )}>
40 + {tab}
41 + </button>
42 + );
43 + })}
44 + </div>
45 + <div className="flex-1 overflow-hidden w-full h-full">
46 + {tabs.get(activeTab)}
47 + </div>
48 </div>
49 );
50 }
compiler/apps/playground/playwright.config.js
+5 -1
@@ -55,12 +55,16 @@ export default defineConfig({
55 // contextOptions: {
56 // ignoreHTTPSErrors: true,
57 // },
58 + viewport: {width: 1920, height: 1080},
59 },
60
61 projects: [
62 {
63 name: 'chromium',
63 - use: {...devices['Desktop Chrome']},
64 + use: {
65 + ...devices['Desktop Chrome'],
66 + viewport: {width: 1920, height: 1080},
67 + },
68 },
69 // {
70 // name: 'Desktop Firefox',