5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {parse as babelParse, ParseResult} from '@babel/parser';
9
-import * as HermesParser from 'hermes-parser';
10
-import * as t from '@babel/types';
11
-import BabelPluginReactCompiler, {
12
- CompilerError,
8
+import {
9
CompilerErrorDetail,
10
CompilerDiagnostic,
15
- Effect,
16
- ErrorCategory,
17
- parseConfigPragmaForTests,
18
- ValueKind,
19
- type Hook,
20
- PluginOptions,
21
- CompilerPipelineValue,
22
- parsePluginOptions,
23
- printReactiveFunctionWithOutlined,
24
- printFunctionWithOutlined,
25
- type LoggerEvent,
11
} from 'babel-plugin-react-compiler';
27
-import {useDeferredValue, useMemo} from 'react';
12
+import {useDeferredValue, useMemo, useState} from 'react';
13
import {useStore} from '../StoreContext';
14
import ConfigEditor from './ConfigEditor';
15
import Input from './Input';
31
-import {
32
- CompilerOutput,
33
- CompilerTransformOutput,
34
- default as Output,
35
- PrintedCompilerPipelineValue,
36
-} from './Output';
37
-import {transformFromAstSync} from '@babel/core';
38
-
39
-function parseInput(
40
- input: string,
41
- language: 'flow' | 'typescript',
42
-): ParseResult<t.File> {
43
- // Extract the first line to quickly check for custom test directives
44
- if (language === 'flow') {
45
- return HermesParser.parse(input, {
46
- babel: true,
47
- flow: 'all',
48
- sourceType: 'module',
49
- enableExperimentalComponentSyntax: true,
50
- });
51
- } else {
52
- return babelParse(input, {
53
- plugins: ['typescript', 'jsx'],
54
- sourceType: 'module',
55
- }) as ParseResult<t.File>;
56
- }
57
-}
58
-
59
-function invokeCompiler(
60
- source: string,
61
- language: 'flow' | 'typescript',
62
- options: PluginOptions,
63
-): CompilerTransformOutput {
64
- const ast = parseInput(source, language);
65
- let result = transformFromAstSync(ast, source, {
66
- filename: '_playgroundFile.js',
67
- highlightCode: false,
68
- retainLines: true,
69
- plugins: [[BabelPluginReactCompiler, options]],
70
- ast: true,
71
- sourceType: 'module',
72
- configFile: false,
73
- sourceMaps: true,
74
- babelrc: false,
75
- });
76
- if (result?.ast == null || result?.code == null || result?.map == null) {
77
- throw new Error('Expected successful compilation');
78
- }
79
- return {
80
- code: result.code,
81
- sourceMaps: result.map,
82
- language,
83
- };
84
-}
85
-
86
-const COMMON_HOOKS: Array<[string, Hook]> = [
87
- [
88
- 'useFragment',
89
- {
90
- valueKind: ValueKind.Frozen,
91
- effectKind: Effect.Freeze,
92
- noAlias: true,
93
- transitiveMixedData: true,
94
- },
95
- ],
96
- [
97
- 'usePaginationFragment',
98
- {
99
- valueKind: ValueKind.Frozen,
100
- effectKind: Effect.Freeze,
101
- noAlias: true,
102
- transitiveMixedData: true,
103
- },
104
- ],
105
- [
106
- 'useRefetchableFragment',
107
- {
108
- valueKind: ValueKind.Frozen,
109
- effectKind: Effect.Freeze,
110
- noAlias: true,
111
- transitiveMixedData: true,
112
- },
113
- ],
114
- [
115
- 'useLazyLoadQuery',
116
- {
117
- valueKind: ValueKind.Frozen,
118
- effectKind: Effect.Freeze,
119
- noAlias: true,
120
- transitiveMixedData: true,
121
- },
122
- ],
123
- [
124
- 'usePreloadedQuery',
125
- {
126
- valueKind: ValueKind.Frozen,
127
- effectKind: Effect.Freeze,
128
- noAlias: true,
129
- transitiveMixedData: true,
130
- },
131
- ],
132
-];
133
-
134
-function parseOptions(
135
- source: string,
136
- mode: 'compiler' | 'linter',
137
- configOverrides: string,
138
-): PluginOptions {
139
- // Extract the first line to quickly check for custom test directives
140
- const pragma = source.substring(0, source.indexOf('\n'));
141
-
142
- const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
143
- compilationMode: 'infer',
144
- environment:
145
- mode === 'linter'
146
- ? {
147
- // enabled in compiler
148
- validateRefAccessDuringRender: false,
149
- // enabled in linter
150
- validateNoSetStateInRender: true,
151
- validateNoSetStateInEffects: true,
152
- validateNoJSXInTryStatements: true,
153
- validateNoImpureFunctionsInRender: true,
154
- validateStaticComponents: true,
155
- validateNoFreezingKnownMutableFunctions: true,
156
- validateNoVoidUseMemo: true,
157
- }
158
- : {
159
- /* use defaults for compiler mode */
160
- },
161
- });
162
-
163
- // Parse config overrides from config editor
164
- let configOverrideOptions: any = {};
165
- const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
166
- if (configOverrides.trim()) {
167
- if (configMatch && configMatch[1]) {
168
- const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
169
- configOverrideOptions = new Function(`return (${configString})`)();
170
- } else {
171
- throw new Error('Invalid override format');
172
- }
173
- }
174
-
175
- const opts: PluginOptions = parsePluginOptions({
176
- ...parsedPragmaOptions,
177
- ...configOverrideOptions,
178
- environment: {
179
- ...parsedPragmaOptions.environment,
180
- ...configOverrideOptions.environment,
181
- customHooks: new Map([...COMMON_HOOKS]),
182
- },
183
- });
184
-
185
- return opts;
186
-}
187
-
188
-function compile(
189
- source: string,
190
- mode: 'compiler' | 'linter',
191
- configOverrides: string,
192
-): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
193
- const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
194
- const error = new CompilerError();
195
- const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
196
- const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
197
- const entry = results.get(result.name);
198
- if (Array.isArray(entry)) {
199
- entry.push(result);
200
- } else {
201
- results.set(result.name, [result]);
202
- }
203
- };
204
- let language: 'flow' | 'typescript';
205
- if (source.match(/\@flow/)) {
206
- language = 'flow';
207
- } else {
208
- language = 'typescript';
209
- }
210
- let transformOutput;
211
-
212
- let baseOpts: PluginOptions | null = null;
213
- try {
214
- baseOpts = parseOptions(source, mode, configOverrides);
215
- } catch (err) {
216
- error.details.push(
217
- new CompilerErrorDetail({
218
- category: ErrorCategory.Config,
219
- reason: `Unexpected failure when transforming configs! \n${err}`,
220
- loc: null,
221
- suggestions: null,
222
- }),
223
- );
224
- }
225
- if (baseOpts) {
226
- try {
227
- const logIR = (result: CompilerPipelineValue): void => {
228
- switch (result.kind) {
229
- case 'ast': {
230
- break;
231
- }
232
- case 'hir': {
233
- upsert({
234
- kind: 'hir',
235
- fnName: result.value.id,
236
- name: result.name,
237
- value: printFunctionWithOutlined(result.value),
238
- });
239
- break;
240
- }
241
- case 'reactive': {
242
- upsert({
243
- kind: 'reactive',
244
- fnName: result.value.id,
245
- name: result.name,
246
- value: printReactiveFunctionWithOutlined(result.value),
247
- });
248
- break;
249
- }
250
- case 'debug': {
251
- upsert({
252
- kind: 'debug',
253
- fnName: null,
254
- name: result.name,
255
- value: result.value,
256
- });
257
- break;
258
- }
259
- default: {
260
- const _: never = result;
261
- throw new Error(`Unhandled result ${result}`);
262
- }
263
- }
264
- };
265
- // Add logger options to the parsed options
266
- const opts = {
267
- ...baseOpts,
268
- logger: {
269
- debugLogIRs: logIR,
270
- logEvent: (_filename: string | null, event: LoggerEvent): void => {
271
- if (event.kind === 'CompileError') {
272
- otherErrors.push(event.detail);
273
- }
274
- },
275
- },
276
- };
277
- transformOutput = invokeCompiler(source, language, opts);
278
- } catch (err) {
279
- /**
280
- * error might be an invariant violation or other runtime error
281
- * (i.e. object shape that is not CompilerError)
282
- */
283
- if (err instanceof CompilerError && err.details.length > 0) {
284
- error.merge(err);
285
- } else {
286
- /**
287
- * Handle unexpected failures by logging (to get a stack trace)
288
- * and reporting
289
- */
290
- error.details.push(
291
- new CompilerErrorDetail({
292
- category: ErrorCategory.Invariant,
293
- reason: `Unexpected failure when transforming input! \n${err}`,
294
- loc: null,
295
- suggestions: null,
296
- }),
297
- );
298
- }
299
- }
300
- }
301
- // Only include logger errors if there weren't other errors
302
- if (!error.hasErrors() && otherErrors.length !== 0) {
303
- otherErrors.forEach(e => error.details.push(e));
304
- }
305
- if (error.hasErrors()) {
306
- return [{kind: 'err', results, error}, language, baseOpts];
307
- }
308
- return [
309
- {kind: 'ok', results, transformOutput, errors: error.details},
310
- language,
311
- baseOpts,
312
- ];
313
-}
16
+import {CompilerOutput, default as Output} from './Output';
17
+import {compile} from '../../lib/compilation';
18
+import prettyFormat from 'pretty-format';
19
20
export default function Editor(): JSX.Element {
21
const store = useStore();
28
() => compile(deferredStore.source, 'linter', deferredStore.config),
29
[deferredStore.source, deferredStore.config],
30
);
31
+ const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
32
33
let mergedOutput: CompilerOutput;
34
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
42
mergedOutput = compilerOutput;
43
errors = compilerOutput.error.details;
44
}
45
+
46
+ if (appliedOptions) {
47
+ const formatted = prettyFormat(appliedOptions, {
48
+ printFunctionName: false,
49
+ printBasicPrototype: false,
50
+ });
51
+ if (formatted !== formattedAppliedConfig) {
52
+ setFormattedAppliedConfig(formatted);
53
+ }
54
+ }
55
+
56
return (
57
<>
58
<div className="relative flex top-14">
59
<div className="flex-shrink-0">
343
- <ConfigEditor appliedOptions={appliedOptions} />
60
+ <ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
61
</div>
62
<div className="flex flex-1 min-w-0">
63
<Input language={language} errors={errors} />