[babel] Compile individual components
Updates the Babel plugin so that we can individually compile components and skip over ones that have non-critical errors
Lauren Tan committed
Apr 20, 2023 at 16:21 UTC
8bab0bca3c1668afadd7c7e7261f934ee0b7ee18
6 files changed
+198
-112
compiler/forget/src/Babel/BabelPlugin.ts
+108
-108
@@ -54,48 +54,67 @@ export default function ReactForgetBabelPlugin(
54
fn: BabelCore.NodePath<t.FunctionDeclaration>,
55
pass: BabelPluginPass
56
): void {
57
- const compiled = compile(fn, pass.opts.environment);
58
-
59
- if (pass.opts.gating != null) {
60
- // Rename existing function
61
- if (fn.node.id == null) {
62
- CompilerError.invariant(
63
- "FunctionDeclaration must have a name",
64
- fn.node.loc ?? GeneratedSource
57
+ try {
58
+ const compiled = compile(fn, pass.opts.environment);
59
+
60
+ if (pass.opts.gating != null) {
61
+ // Rename existing function
62
+ if (fn.node.id == null) {
63
+ CompilerError.invariant(
64
+ "FunctionDeclaration must have a name",
65
+ fn.node.loc ?? GeneratedSource
66
+ );
67
+ }
68
+ const original = fn.node.id;
69
+ fn.node.id = addSuffix(fn.node.id, "_uncompiled");
70
+
71
+ // Rename and append compiled function
72
+ if (compiled.id == null) {
73
+ CompilerError.invariant(
74
+ "FunctionDeclaration must produce a name",
75
+ fn.node.loc ?? GeneratedSource
76
+ );
77
+ }
78
+ compiled.id = addSuffix(compiled.id, "_forget");
79
+ const compiledFn = fn.insertAfter(compiled)[0];
80
+ compiledFn.skip();
81
+
82
+ // Build and append gating test
83
+ compiledFn.insertAfter(
84
+ buildGatingTest({
85
+ originalFnDecl: fn,
86
+ compiledIdent: compiled.id,
87
+ originalIdent: original,
88
+ gating: pass.opts.gating,
89
+ })
90
);
91
+ } else {
92
+ fn.replaceWith(compiled);
93
}
67
- const original = fn.node.id;
68
- fn.node.id = addSuffix(fn.node.id, "_uncompiled");
69
-
70
- // Rename and append compiled function
71
- if (compiled.id == null) {
72
- CompilerError.invariant(
73
- "FunctionDeclaration must produce a name",
74
- fn.node.loc ?? GeneratedSource
75
- );
94
+
95
+ hasForgetCompiledCode = true;
96
+ } catch (err) {
97
+ if (pass.opts.logger && err) {
98
+ pass.opts.logger.logEvent("err", err);
99
+ }
100
+ /** Always throw if the flag is enabled, otherwise we only throw if the error is critical
101
+ * (eg an invariant is broken, meaning the compiler may be buggy). See
102
+ * {@link CompilerError.isCritical} for mappings.
103
+ * */
104
+ if (
105
+ pass.opts.panicOnBailout ||
106
+ !(err instanceof CompilerError) ||
107
+ (err instanceof CompilerError && err.isCritical())
108
+ ) {
109
+ throw err;
110
+ } else {
111
+ console.error(err);
112
}
77
- compiled.id = addSuffix(compiled.id, "_forget");
78
- const compiledFn = fn.insertAfter(compiled)[0];
79
- compiledFn.skip();
80
-
81
- // Build and append gating test
82
- compiledFn.insertAfter(
83
- buildGatingTest({
84
- originalFnDecl: fn,
85
- compiledIdent: compiled.id,
86
- originalIdent: original,
87
- gating: pass.opts.gating,
88
- })
89
- );
90
- } else {
91
- fn.replaceWith(compiled);
113
+ } finally {
114
+ // We are generating a new FunctionDeclaration node, so we must skip over it or this
115
+ // traversal will loop infinitely.
116
+ fn.skip();
117
}
93
-
94
- hasForgetCompiledCode = true;
95
-
96
- // We are generating a new FunctionDeclaration node, so we must skip over it or this
97
- // traversal will loop infinitely.
98
- fn.skip();
118
}
119
120
const visitor = {
@@ -190,78 +209,59 @@ export default function ReactForgetBabelPlugin(
209
return;
210
}
211
193
- try {
194
- path.traverse(visitor, {
195
- ...pass,
196
- opts: { ...pass.opts, ...options },
197
- });
212
+ path.traverse(visitor, {
213
+ ...pass,
214
+ opts: { ...pass.opts, ...options },
215
+ });
216
199
- // If there isn't already an import of * as React, insert it so React.useMemoCache doesn't
200
- // throw
201
- if (hasForgetCompiledCode) {
202
- let didInsertUseMemoCache = false;
203
- let hasExistingReactImport = false;
204
- path.traverse({
205
- MemberExpression(memberExprPath) {
206
- const obj = memberExprPath.get("object");
207
- const prop = memberExprPath.get("property");
208
- if (
209
- obj.isIdentifier() &&
210
- obj.node.name === "React" &&
211
- prop.isIdentifier() &&
212
- prop.node.name === "unstable_useMemoCache"
213
- ) {
214
- didInsertUseMemoCache = true;
215
- memberExprPath.stop();
216
- }
217
- },
218
- ImportDeclaration(importDeclPath) {
219
- if (
220
- importDeclPath.get("source").node.value === "react" &&
221
- importDeclPath.get("specifiers").length === 1 &&
222
- importDeclPath
223
- .get("specifiers")[0]
224
- .isImportNamespaceSpecifier() &&
225
- importDeclPath.get("specifiers")[0].get("local").node.name ===
226
- "React"
227
- ) {
228
- hasExistingReactImport = true;
229
- importDeclPath.stop();
230
- }
231
- },
232
- });
233
- if (didInsertUseMemoCache && !hasExistingReactImport) {
234
- path.unshiftContainer(
235
- "body",
236
- t.importDeclaration(
237
- [t.importNamespaceSpecifier(t.identifier("React"))],
238
- t.stringLiteral("react")
239
- )
240
- );
241
- }
242
- if (options.gating != null) {
243
- path.unshiftContainer(
244
- "body",
245
- buildImportForGatingModule(options.gating)
246
- );
247
- }
248
- }
249
- } catch (err) {
250
- if (options.logger && err) {
251
- options.logger.logEvent("err", err);
217
+ // If there isn't already an import of * as React, insert it so React.useMemoCache doesn't
218
+ // throw
219
+ if (hasForgetCompiledCode) {
220
+ let didInsertUseMemoCache = false;
221
+ let hasExistingReactImport = false;
222
+ path.traverse({
223
+ MemberExpression(memberExprPath) {
224
+ const obj = memberExprPath.get("object");
225
+ const prop = memberExprPath.get("property");
226
+ if (
227
+ obj.isIdentifier() &&
228
+ obj.node.name === "React" &&
229
+ prop.isIdentifier() &&
230
+ prop.node.name === "unstable_useMemoCache"
231
+ ) {
232
+ didInsertUseMemoCache = true;
233
+ memberExprPath.stop();
234
+ }
235
+ },
236
+ ImportDeclaration(importDeclPath) {
237
+ if (
238
+ importDeclPath.get("source").node.value === "react" &&
239
+ importDeclPath.get("specifiers").length === 1 &&
240
+ importDeclPath
241
+ .get("specifiers")[0]
242
+ .isImportNamespaceSpecifier() &&
243
+ importDeclPath.get("specifiers")[0].get("local").node.name ===
244
+ "React"
245
+ ) {
246
+ hasExistingReactImport = true;
247
+ importDeclPath.stop();
248
+ }
249
+ },
250
+ });
251
+ if (didInsertUseMemoCache && !hasExistingReactImport) {
252
+ path.unshiftContainer(
253
+ "body",
254
+ t.importDeclaration(
255
+ [t.importNamespaceSpecifier(t.identifier("React"))],
256
+ t.stringLiteral("react")
257
+ )
258
+ );
259
}
253
- /** Always throw if the flag is enabled, otherwise we only throw if the error is critical
254
- * (eg an invariant is broken, meaning the compiler may be buggy). See
255
- * {@link CompilerError.isCritical} for mappings.
256
- * */
257
- if (
258
- options.panicOnBailout ||
259
- !(err instanceof CompilerError) ||
260
- (err instanceof CompilerError && err.isCritical())
261
- ) {
262
- throw err;
263
- } else {
264
- console.error(err);
260
+ if (options.gating != null) {
261
+ path.unshiftContainer(
262
+ "body",
263
+ buildImportForGatingModule(options.gating)
264
+ );
265
}
266
}
267
},
compiler/forget/src/__tests__/compiler-test.ts
+20
-2
@@ -29,14 +29,20 @@ wasmFolder(
29
);
30
31
describe("React Forget", () => {
32
+ const originalConsoleError = console.error;
33
generateTestsFromFixtures(
34
path.join(__dirname, "fixtures", "compiler"),
35
(input, file, options) => {
36
+ const seenConsoleErrors: Array<string> = [];
37
let items: Array<TestOutput> = [];
38
let error: Error | null = null;
39
if (options.debug) {
40
toggleLogging(options.debug);
41
}
42
+ // Mock console.error so we can record it in test output
43
+ console.error = jest.fn((...messages: Array<string>) => {
44
+ seenConsoleErrors.push(...messages);
45
+ });
46
try {
47
items.push({
48
js: runReactForgetBabelPlugin(input, file, options.language, {
@@ -58,12 +64,23 @@ describe("React Forget", () => {
64
},
65
logger: null,
66
gating: options.gating,
61
- panicOnBailout: true,
67
+ panicOnBailout: options.panicOnBailout,
68
}).code,
69
});
70
} catch (e) {
71
error = e;
72
}
73
+
74
+ // Promote console errors so they can be recorded in fixture output
75
+ for (const consoleError of seenConsoleErrors) {
76
+ if (error != null) {
77
+ error.message = `${error.message}\n\n${consoleError}`;
78
+ } else {
79
+ error = new Error(consoleError);
80
+ error.name = "ConsoleError";
81
+ }
82
+ }
83
+
84
let outputs: Array<string>;
85
86
const expectError = file.startsWith("error.");
@@ -73,7 +90,7 @@ describe("React Forget", () => {
90
`Expected an error to be thrown for fixture: '${file}', remove the 'error.' prefix if an error is not expected.`
91
);
92
} else {
76
- outputs = [formatErrorOutput(error)];
93
+ outputs = [...formatOutput(items), formatErrorOutput(error)];
94
}
95
} else {
96
if (error !== null) {
@@ -94,6 +111,7 @@ ${outputs.join("\n")}
111
`;
112
}
113
);
114
+ console.error = originalConsoleError;
115
});
116
117
function formatErrorOutput(error: Error): string {
compiler/forget/src/__tests__/disableMemoizeJsxElements-test.ts
+1
-1
@@ -54,7 +54,7 @@ describe("React Forget (Disable memoization of JSX elements)", () => {
54
},
55
logger: null,
56
gating: options.gating,
57
- panicOnBailout: true,
57
+ panicOnBailout: options.panicOnBailout,
58
}).code,
59
});
60
} catch (e) {
compiler/forget/src/__tests__/fixtures/compiler/error.file-has-non-critical-errors.expect.md
new
+54
@@ -0,0 +1,54 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @panicOnBailout false
6
+function Bad() {
7
+ var x = 1;
8
+ return <div>{x}</div>;
9
+}
10
+
11
+function Good() {
12
+ const x = 1;
13
+ return <div>{x}</div>;
14
+}
15
+
16
+```
17
+
18
+## Code
19
+
20
+```javascript
21
+import * as React from "react"; // @panicOnBailout false
22
+function Bad() {
23
+ var x = 1;
24
+ return <div>{x}</div>;
25
+}
26
+
27
+function Good() {
28
+ const $ = React.unstable_useMemoCache(1);
29
+ let t0;
30
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
31
+ t0 = <div>{1}</div>;
32
+ $[0] = t0;
33
+ } else {
34
+ t0 = $[0];
35
+ }
36
+ return t0;
37
+}
38
+
39
+```
40
+
41
+## Error
42
+
43
+```
44
+[ReactForget] TodoError: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
45
+ 1 | // @panicOnBailout false
46
+ 2 | function Bad() {
47
+> 3 | var x = 1;
48
+ | ^^^^^^^^^^
49
+ 4 | return <div>{x}</div>;
50
+ 5 | }
51
+ 6 |
52
+```
53
+
54
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.file-has-non-critical-errors.js
new
+10
@@ -0,0 +1,10 @@
1
+// @panicOnBailout false
2
+function Bad() {
3
+ var x = 1;
4
+ return <div>{x}</div>;
5
+}
6
+
7
+function Good() {
8
+ const x = 1;
9
+ return <div>{x}</div>;
10
+}
compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts
+5
-1
@@ -92,6 +92,7 @@ export default function generateTestsFromFixtures(
92
let enableOnlyOnUseForgetDirective = false;
93
let gating: GatingOptions | null = null;
94
let inlineUseMemo = true;
95
+ let panicOnBailout = true;
96
97
if (inputFile != null) {
98
input = fs.readFileSync(inputFile, "utf8");
@@ -115,6 +116,9 @@ export default function generateTestsFromFixtures(
116
if (lines[0]!.indexOf("@inlineUseMemo") !== -1) {
117
inlineUseMemo = true;
118
}
119
+ if (lines[0]!.indexOf("@panicOnBailout false") !== -1) {
120
+ panicOnBailout = false;
121
+ }
122
}
123
124
testCommand(basename, () => {
@@ -127,7 +131,7 @@ export default function generateTestsFromFixtures(
131
enableOnlyOnUseForgetDirective,
132
gating,
133
language: parseLanguage(input),
130
- panicOnBailout: true,
134
+ panicOnBailout,
135
});
136
} else {
137
receivedOutput = "<<input deleted>>";