[babel] Compile individual components

Updates the Babel plugin so that we can individually compile components and skip 
over ones that have non-critical errors
This commit is contained in:
Lauren Tan
2023-04-20 16:21:40 -04:00
parent c228d63f03
commit 8bab0bca3c
6 changed files with 196 additions and 110 deletions
+106 -106
View File
@@ -54,48 +54,67 @@ export default function ReactForgetBabelPlugin(
fn: BabelCore.NodePath<t.FunctionDeclaration>,
pass: BabelPluginPass
): void {
const compiled = compile(fn, pass.opts.environment);
try {
const compiled = compile(fn, pass.opts.environment);
if (pass.opts.gating != null) {
// Rename existing function
if (fn.node.id == null) {
CompilerError.invariant(
"FunctionDeclaration must have a name",
fn.node.loc ?? GeneratedSource
if (pass.opts.gating != null) {
// Rename existing function
if (fn.node.id == null) {
CompilerError.invariant(
"FunctionDeclaration must have a name",
fn.node.loc ?? GeneratedSource
);
}
const original = fn.node.id;
fn.node.id = addSuffix(fn.node.id, "_uncompiled");
// Rename and append compiled function
if (compiled.id == null) {
CompilerError.invariant(
"FunctionDeclaration must produce a name",
fn.node.loc ?? GeneratedSource
);
}
compiled.id = addSuffix(compiled.id, "_forget");
const compiledFn = fn.insertAfter(compiled)[0];
compiledFn.skip();
// Build and append gating test
compiledFn.insertAfter(
buildGatingTest({
originalFnDecl: fn,
compiledIdent: compiled.id,
originalIdent: original,
gating: pass.opts.gating,
})
);
} else {
fn.replaceWith(compiled);
}
const original = fn.node.id;
fn.node.id = addSuffix(fn.node.id, "_uncompiled");
// Rename and append compiled function
if (compiled.id == null) {
CompilerError.invariant(
"FunctionDeclaration must produce a name",
fn.node.loc ?? GeneratedSource
);
hasForgetCompiledCode = true;
} catch (err) {
if (pass.opts.logger && err) {
pass.opts.logger.logEvent("err", err);
}
compiled.id = addSuffix(compiled.id, "_forget");
const compiledFn = fn.insertAfter(compiled)[0];
compiledFn.skip();
// Build and append gating test
compiledFn.insertAfter(
buildGatingTest({
originalFnDecl: fn,
compiledIdent: compiled.id,
originalIdent: original,
gating: pass.opts.gating,
})
);
} else {
fn.replaceWith(compiled);
/** Always throw if the flag is enabled, otherwise we only throw if the error is critical
* (eg an invariant is broken, meaning the compiler may be buggy). See
* {@link CompilerError.isCritical} for mappings.
* */
if (
pass.opts.panicOnBailout ||
!(err instanceof CompilerError) ||
(err instanceof CompilerError && err.isCritical())
) {
throw err;
} else {
console.error(err);
}
} finally {
// We are generating a new FunctionDeclaration node, so we must skip over it or this
// traversal will loop infinitely.
fn.skip();
}
hasForgetCompiledCode = true;
// We are generating a new FunctionDeclaration node, so we must skip over it or this
// traversal will loop infinitely.
fn.skip();
}
const visitor = {
@@ -190,78 +209,59 @@ export default function ReactForgetBabelPlugin(
return;
}
try {
path.traverse(visitor, {
...pass,
opts: { ...pass.opts, ...options },
});
path.traverse(visitor, {
...pass,
opts: { ...pass.opts, ...options },
});
// If there isn't already an import of * as React, insert it so React.useMemoCache doesn't
// throw
if (hasForgetCompiledCode) {
let didInsertUseMemoCache = false;
let hasExistingReactImport = false;
path.traverse({
MemberExpression(memberExprPath) {
const obj = memberExprPath.get("object");
const prop = memberExprPath.get("property");
if (
obj.isIdentifier() &&
obj.node.name === "React" &&
prop.isIdentifier() &&
prop.node.name === "unstable_useMemoCache"
) {
didInsertUseMemoCache = true;
memberExprPath.stop();
}
},
ImportDeclaration(importDeclPath) {
if (
importDeclPath.get("source").node.value === "react" &&
importDeclPath.get("specifiers").length === 1 &&
importDeclPath
.get("specifiers")[0]
.isImportNamespaceSpecifier() &&
importDeclPath.get("specifiers")[0].get("local").node.name ===
"React"
) {
hasExistingReactImport = true;
importDeclPath.stop();
}
},
});
if (didInsertUseMemoCache && !hasExistingReactImport) {
path.unshiftContainer(
"body",
t.importDeclaration(
[t.importNamespaceSpecifier(t.identifier("React"))],
t.stringLiteral("react")
)
);
}
if (options.gating != null) {
path.unshiftContainer(
"body",
buildImportForGatingModule(options.gating)
);
}
// If there isn't already an import of * as React, insert it so React.useMemoCache doesn't
// throw
if (hasForgetCompiledCode) {
let didInsertUseMemoCache = false;
let hasExistingReactImport = false;
path.traverse({
MemberExpression(memberExprPath) {
const obj = memberExprPath.get("object");
const prop = memberExprPath.get("property");
if (
obj.isIdentifier() &&
obj.node.name === "React" &&
prop.isIdentifier() &&
prop.node.name === "unstable_useMemoCache"
) {
didInsertUseMemoCache = true;
memberExprPath.stop();
}
},
ImportDeclaration(importDeclPath) {
if (
importDeclPath.get("source").node.value === "react" &&
importDeclPath.get("specifiers").length === 1 &&
importDeclPath
.get("specifiers")[0]
.isImportNamespaceSpecifier() &&
importDeclPath.get("specifiers")[0].get("local").node.name ===
"React"
) {
hasExistingReactImport = true;
importDeclPath.stop();
}
},
});
if (didInsertUseMemoCache && !hasExistingReactImport) {
path.unshiftContainer(
"body",
t.importDeclaration(
[t.importNamespaceSpecifier(t.identifier("React"))],
t.stringLiteral("react")
)
);
}
} catch (err) {
if (options.logger && err) {
options.logger.logEvent("err", err);
}
/** Always throw if the flag is enabled, otherwise we only throw if the error is critical
* (eg an invariant is broken, meaning the compiler may be buggy). See
* {@link CompilerError.isCritical} for mappings.
* */
if (
options.panicOnBailout ||
!(err instanceof CompilerError) ||
(err instanceof CompilerError && err.isCritical())
) {
throw err;
} else {
console.error(err);
if (options.gating != null) {
path.unshiftContainer(
"body",
buildImportForGatingModule(options.gating)
);
}
}
},
+20 -2
View File
@@ -29,14 +29,20 @@ wasmFolder(
);
describe("React Forget", () => {
const originalConsoleError = console.error;
generateTestsFromFixtures(
path.join(__dirname, "fixtures", "compiler"),
(input, file, options) => {
const seenConsoleErrors: Array<string> = [];
let items: Array<TestOutput> = [];
let error: Error | null = null;
if (options.debug) {
toggleLogging(options.debug);
}
// Mock console.error so we can record it in test output
console.error = jest.fn((...messages: Array<string>) => {
seenConsoleErrors.push(...messages);
});
try {
items.push({
js: runReactForgetBabelPlugin(input, file, options.language, {
@@ -58,12 +64,23 @@ describe("React Forget", () => {
},
logger: null,
gating: options.gating,
panicOnBailout: true,
panicOnBailout: options.panicOnBailout,
}).code,
});
} catch (e) {
error = e;
}
// Promote console errors so they can be recorded in fixture output
for (const consoleError of seenConsoleErrors) {
if (error != null) {
error.message = `${error.message}\n\n${consoleError}`;
} else {
error = new Error(consoleError);
error.name = "ConsoleError";
}
}
let outputs: Array<string>;
const expectError = file.startsWith("error.");
@@ -73,7 +90,7 @@ describe("React Forget", () => {
`Expected an error to be thrown for fixture: '${file}', remove the 'error.' prefix if an error is not expected.`
);
} else {
outputs = [formatErrorOutput(error)];
outputs = [...formatOutput(items), formatErrorOutput(error)];
}
} else {
if (error !== null) {
@@ -94,6 +111,7 @@ ${outputs.join("\n")}
`;
}
);
console.error = originalConsoleError;
});
function formatErrorOutput(error: Error): string {
@@ -54,7 +54,7 @@ describe("React Forget (Disable memoization of JSX elements)", () => {
},
logger: null,
gating: options.gating,
panicOnBailout: true,
panicOnBailout: options.panicOnBailout,
}).code,
});
} catch (e) {
@@ -0,0 +1,54 @@
## Input
```javascript
// @panicOnBailout false
function Bad() {
var x = 1;
return <div>{x}</div>;
}
function Good() {
const x = 1;
return <div>{x}</div>;
}
```
## Code
```javascript
import * as React from "react"; // @panicOnBailout false
function Bad() {
var x = 1;
return <div>{x}</div>;
}
function Good() {
const $ = React.unstable_useMemoCache(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div>{1}</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
```
## Error
```
[ReactForget] TodoError: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
1 | // @panicOnBailout false
2 | function Bad() {
> 3 | var x = 1;
| ^^^^^^^^^^
4 | return <div>{x}</div>;
5 | }
6 |
```
@@ -0,0 +1,10 @@
// @panicOnBailout false
function Bad() {
var x = 1;
return <div>{x}</div>;
}
function Good() {
const x = 1;
return <div>{x}</div>;
}
@@ -92,6 +92,7 @@ export default function generateTestsFromFixtures(
let enableOnlyOnUseForgetDirective = false;
let gating: GatingOptions | null = null;
let inlineUseMemo = true;
let panicOnBailout = true;
if (inputFile != null) {
input = fs.readFileSync(inputFile, "utf8");
@@ -115,6 +116,9 @@ export default function generateTestsFromFixtures(
if (lines[0]!.indexOf("@inlineUseMemo") !== -1) {
inlineUseMemo = true;
}
if (lines[0]!.indexOf("@panicOnBailout false") !== -1) {
panicOnBailout = false;
}
}
testCommand(basename, () => {
@@ -127,7 +131,7 @@ export default function generateTestsFromFixtures(
enableOnlyOnUseForgetDirective,
gating,
language: parseLanguage(input),
panicOnBailout: true,
panicOnBailout,
});
} else {
receivedOutput = "<<input deleted>>";