@samitouri / QOS-React / commits / 8bab0bca3c

[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>>";