mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[compiler][be] Playground now compiles entire program
Compiler playground now runs the entire program through `babel-plugin-react-compiler` instead of a custom pipeline which previously duplicated function inference logic from `Program.ts`. In addition, the playground output reflects the tranformed file (instead of a "virtual file" of manually concatenated functions). This helps with the following: - Reduce potential discrepencies between playground and babel plugin behavior. See attached fixture output for an example where we previously diverged. - Let playground users see compiler-inserted imports (e.g. `_c` or `useFire`) This also helps us repurpose playground into a more general tool for compiler-users instead of just for compiler engineers. - imports and other functions are preserved. We differentiate between imports and globals in many cases (e.g. `inferEffectDeps`), so it may be misleading to omit imports in printed output - playground now shows other program-changing behavior like position of outlined functions and hoisted declarations - emitted compiled functions do not need synthetic names
This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
||||
function TestComponent(t0) {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
function MyApp() {
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function MyApp() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
function TestComponent(t0) {
|
||||
"use memo";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
export default function TestComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
let t1;
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
function TestComponent({ x }) {
|
||||
"use no memo";
|
||||
export default function TestComponent({ x }) {
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
"use no memo";
|
||||
function TestComponent({ x }) {
|
||||
"use memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function TestComponent(t0) {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
@@ -12,7 +13,7 @@ function TestComponent(t0) {
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function anonymous_1(t0) {
|
||||
const TestComponent2 = (t0) => {
|
||||
"use memo";
|
||||
const $ = _c(2);
|
||||
const { x } = t0;
|
||||
@@ -25,4 +26,4 @@ function anonymous_1(t0) {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
function anonymous_1() {
|
||||
const TestComponent = function () {
|
||||
"use no memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
function anonymous_3({ x }) {
|
||||
};
|
||||
const TestComponent2 = ({ x }) => {
|
||||
"use no memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ const TestComponent2 = ({ x }) => {
|
||||
};`,
|
||||
},
|
||||
{
|
||||
name: 'function-scope-beats-module-scope',
|
||||
name: 'todo-function-scope-does-not-beat-module-scope',
|
||||
input: `
|
||||
'use no memo';
|
||||
function TestComponent({ x }) {
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parse as babelParse} from '@babel/parser';
|
||||
import {parse as babelParse, ParseResult} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import traverse, {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
import BabelPluginReactCompiler, {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragmaForTests,
|
||||
ValueKind,
|
||||
runPlayground,
|
||||
type Hook,
|
||||
findDirectiveDisablingMemoization,
|
||||
findDirectiveEnablingMemoization,
|
||||
PluginOptions,
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import {type EnvironmentConfig} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
@@ -39,32 +38,18 @@ import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import Input from './Input';
|
||||
import {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
default as Output,
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
|
||||
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
|
||||
type FunctionLike =
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>;
|
||||
enum MemoizeDirectiveState {
|
||||
Enabled = 'Enabled',
|
||||
Disabled = 'Disabled',
|
||||
Undefined = 'Undefined',
|
||||
}
|
||||
|
||||
const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
|
||||
MemoizeDirectiveState.Enabled,
|
||||
MemoizeDirectiveState.Undefined,
|
||||
]);
|
||||
|
||||
const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
|
||||
MemoizeDirectiveState.Enabled,
|
||||
MemoizeDirectiveState.Disabled,
|
||||
]);
|
||||
function parseInput(input: string, language: 'flow' | 'typescript'): any {
|
||||
function parseInput(
|
||||
input: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): ParseResult<t.File> {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
@@ -77,95 +62,45 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
|
||||
return babelParse(input, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
sourceType: 'module',
|
||||
});
|
||||
}) as ParseResult<t.File>;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFunctions(
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): Array<{
|
||||
compilationEnabled: boolean;
|
||||
fn: FunctionLike;
|
||||
}> {
|
||||
const items: Array<{
|
||||
compilationEnabled: boolean;
|
||||
fn: FunctionLike;
|
||||
}> = [];
|
||||
try {
|
||||
const ast = parseInput(source, language);
|
||||
traverse(ast, {
|
||||
FunctionDeclaration(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
ArrowFunctionExpression(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
FunctionExpression(nodePath) {
|
||||
items.push({
|
||||
compilationEnabled: shouldCompile(nodePath),
|
||||
fn: nodePath,
|
||||
});
|
||||
nodePath.skip();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
CompilerError.throwInvalidJS({
|
||||
reason: String(e),
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
environment: EnvironmentConfig,
|
||||
logIR: (pipelineValue: CompilerPipelineValue) => void,
|
||||
): CompilerTransformOutput {
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
},
|
||||
environment,
|
||||
compilationMode: 'all',
|
||||
panicThreshold: 'all_errors',
|
||||
});
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, opts]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
sourceMaps: true,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.ast == null || result?.code == null || result?.map == null) {
|
||||
throw new Error('Expected successful compilation');
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function shouldCompile(fn: FunctionLike): boolean {
|
||||
const {body} = fn.node;
|
||||
if (t.isBlockStatement(body)) {
|
||||
const selfCheck = checkExplicitMemoizeDirectives(body.directives);
|
||||
if (selfCheck === MemoizeDirectiveState.Enabled) return true;
|
||||
if (selfCheck === MemoizeDirectiveState.Disabled) return false;
|
||||
|
||||
const parentWithDirective = fn.findParent(parentPath => {
|
||||
if (parentPath.isBlockStatement() || parentPath.isProgram()) {
|
||||
const directiveCheck = checkExplicitMemoizeDirectives(
|
||||
parentPath.node.directives,
|
||||
);
|
||||
return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!parentWithDirective) return true;
|
||||
const parentDirectiveCheck = checkExplicitMemoizeDirectives(
|
||||
(parentWithDirective.node as t.Program | t.BlockStatement).directives,
|
||||
);
|
||||
return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkExplicitMemoizeDirectives(
|
||||
directives: Array<t.Directive>,
|
||||
): MemoizeDirectiveState {
|
||||
if (findDirectiveEnablingMemoization(directives).length) {
|
||||
return MemoizeDirectiveState.Enabled;
|
||||
}
|
||||
if (findDirectiveDisablingMemoization(directives).length) {
|
||||
return MemoizeDirectiveState.Disabled;
|
||||
}
|
||||
return MemoizeDirectiveState.Undefined;
|
||||
return {
|
||||
code: result.code,
|
||||
sourceMaps: result.map,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
@@ -216,37 +151,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
function isHookName(s: string): boolean {
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
|
||||
if (id != null) {
|
||||
if (isHookName(id.name)) {
|
||||
return 'Hook';
|
||||
}
|
||||
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
if (isPascalCaseNameSpace.test(id.name)) {
|
||||
return 'Component';
|
||||
}
|
||||
}
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
function getFunctionIdentifier(
|
||||
fn:
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>,
|
||||
): t.Identifier | null {
|
||||
if (fn.isArrowFunctionExpression()) {
|
||||
return null;
|
||||
}
|
||||
const id = fn.get('id');
|
||||
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
|
||||
}
|
||||
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
@@ -264,71 +168,25 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let count = 0;
|
||||
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
|
||||
if (id != null && id.name != null) {
|
||||
return id;
|
||||
} else {
|
||||
return t.identifier(`anonymous_${count++}`);
|
||||
}
|
||||
};
|
||||
let transformOutput;
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const config = parseConfigPragmaForTests(pragma);
|
||||
const parsedFunctions = parseFunctions(source, language);
|
||||
for (const func of parsedFunctions) {
|
||||
const id = withIdentifier(getFunctionIdentifier(func.fn));
|
||||
const fnName = id.name;
|
||||
if (!func.compilationEnabled) {
|
||||
upsert({
|
||||
kind: 'ast',
|
||||
fnName,
|
||||
name: 'CodeGen',
|
||||
value: {
|
||||
type: 'FunctionDeclaration',
|
||||
id:
|
||||
func.fn.isArrowFunctionExpression() ||
|
||||
func.fn.isFunctionExpression()
|
||||
? withIdentifier(null)
|
||||
: func.fn.node.id,
|
||||
async: func.fn.node.async,
|
||||
generator: !!func.fn.node.generator,
|
||||
body: func.fn.node.body as t.BlockStatement,
|
||||
params: func.fn.node.params,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const result of runPlayground(
|
||||
func.fn,
|
||||
{
|
||||
...config,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
getReactFunctionType(id),
|
||||
)) {
|
||||
|
||||
transformOutput = invokeCompiler(
|
||||
source,
|
||||
language,
|
||||
{...config, customHooks: new Map([...COMMON_HOOKS])},
|
||||
result => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
upsert({
|
||||
kind: 'ast',
|
||||
fnName,
|
||||
name: result.name,
|
||||
value: {
|
||||
type: 'FunctionDeclaration',
|
||||
id: withIdentifier(result.value.id),
|
||||
async: result.value.async,
|
||||
generator: result.value.generator,
|
||||
body: result.value.body,
|
||||
params: result.value.params,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName,
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
@@ -337,7 +195,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName,
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
@@ -346,7 +204,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName,
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
@@ -357,8 +215,8 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
@@ -385,7 +243,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error: error}, language];
|
||||
}
|
||||
return [{kind: 'ok', results}, language];
|
||||
return [{kind: 'ok', results, transformOutput}, language];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
@@ -405,7 +263,7 @@ export default function Editor(): JSX.Element {
|
||||
} catch (e) {
|
||||
invariant(e instanceof Error, 'Only Error may be caught.');
|
||||
enqueueSnackbar(e.message, {
|
||||
variant: 'message',
|
||||
variant: 'warning',
|
||||
...createMessage(
|
||||
'Bad URL - fell back to the default Playground.',
|
||||
MessageLevel.Info,
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CodeIcon,
|
||||
DocumentAddIcon,
|
||||
@@ -21,17 +19,12 @@ import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
const MemoizedOutput = memo(Output);
|
||||
|
||||
export default MemoizedOutput;
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'ast';
|
||||
name: string;
|
||||
fnName: string | null;
|
||||
value: t.FunctionDeclaration;
|
||||
}
|
||||
| {
|
||||
kind: 'hir';
|
||||
name: string;
|
||||
@@ -41,8 +34,17 @@ export type PrintedCompilerPipelineValue =
|
||||
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
|
||||
| {kind: 'debug'; name: string; fnName: string | null; value: string};
|
||||
|
||||
export type CompilerTransformOutput = {
|
||||
code: string;
|
||||
sourceMaps: BabelFileResult['map'];
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
export type CompilerOutput =
|
||||
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
|
||||
| {
|
||||
kind: 'ok';
|
||||
transformOutput: CompilerTransformOutput;
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
}
|
||||
| {
|
||||
kind: 'err';
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
@@ -61,7 +63,6 @@ async function tabify(
|
||||
const tabs = new Map<string, React.ReactNode>();
|
||||
const reorderedTabs = new Map<string, React.ReactNode>();
|
||||
const concattedResults = new Map<string, string>();
|
||||
let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
|
||||
// Concat all top level function declaration results into a single tab for each pass
|
||||
for (const [passName, results] of compilerOutput.results) {
|
||||
for (const result of results) {
|
||||
@@ -87,9 +88,6 @@ async function tabify(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ast':
|
||||
topLevelFnDecls.push(result.value);
|
||||
break;
|
||||
case 'debug': {
|
||||
concattedResults.set(passName, result.value);
|
||||
break;
|
||||
@@ -114,13 +112,17 @@ async function tabify(
|
||||
lastPassOutput = text;
|
||||
}
|
||||
// Ensure that JS and the JS source map come first
|
||||
if (topLevelFnDecls.length > 0) {
|
||||
/**
|
||||
* Make a synthetic Program so we can have a single AST with all the top level
|
||||
* FunctionDeclarations
|
||||
*/
|
||||
const ast = t.program(topLevelFnDecls);
|
||||
const {code, sourceMapUrl} = await codegen(ast, source);
|
||||
if (compilerOutput.kind === 'ok') {
|
||||
const {transformOutput} = compilerOutput;
|
||||
const sourceMapUrl = getSourceMapUrl(
|
||||
transformOutput.code,
|
||||
JSON.stringify(transformOutput.sourceMaps),
|
||||
);
|
||||
const code = await prettier.format(transformOutput.code, {
|
||||
semi: true,
|
||||
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
reorderedTabs.set(
|
||||
'JS',
|
||||
<TextTabContent
|
||||
@@ -147,27 +149,6 @@ async function tabify(
|
||||
return reorderedTabs;
|
||||
}
|
||||
|
||||
async function codegen(
|
||||
ast: t.Program,
|
||||
source: string,
|
||||
): Promise<{code: any; sourceMapUrl: string | null}> {
|
||||
const generated = generate(
|
||||
ast,
|
||||
{sourceMaps: true, sourceFileName: 'input.js'},
|
||||
source,
|
||||
);
|
||||
const sourceMapUrl = getSourceMapUrl(
|
||||
generated.code,
|
||||
JSON.stringify(generated.map),
|
||||
);
|
||||
const codegenOutput = await prettier.format(generated.code, {
|
||||
semi: true,
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel, prettierPluginEstree],
|
||||
});
|
||||
return {code: codegenOutput, sourceMapUrl};
|
||||
}
|
||||
|
||||
function utf16ToUTF8(s: string): string {
|
||||
return unescape(encodeURIComponent(s));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,10 @@ export default function BabelPluginReactCompiler(
|
||||
) {
|
||||
opts = injectReanimatedFlag(opts);
|
||||
}
|
||||
if (isDev) {
|
||||
if (
|
||||
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
|
||||
isDev
|
||||
) {
|
||||
opts = {
|
||||
...opts,
|
||||
environment: {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerPipelineValue} from './Pipeline';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
/*
|
||||
@@ -209,6 +210,7 @@ export type LoggerEvent =
|
||||
|
||||
export type Logger = {
|
||||
logEvent: (filename: string | null, event: LoggerEvent) => void;
|
||||
debugLogIRs?: (value: CompilerPipelineValue) => void;
|
||||
};
|
||||
|
||||
export const defaultOptions: PluginOptions = {
|
||||
|
||||
@@ -112,7 +112,7 @@ export type CompilerPipelineValue =
|
||||
| {kind: 'reactive'; name: string; value: ReactiveFunction}
|
||||
| {kind: 'debug'; name: string; value: string};
|
||||
|
||||
export function* run(
|
||||
function run(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
@@ -122,7 +122,7 @@ export function* run(
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
): CodegenFunction {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(
|
||||
func.scope,
|
||||
@@ -134,12 +134,17 @@ export function* run(
|
||||
code,
|
||||
useMemoCacheIdentifier,
|
||||
);
|
||||
yield log({
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
name: 'EnvironmentConfig',
|
||||
value: prettyFormat(env.config),
|
||||
});
|
||||
const ast = yield* runWithEnvironment(func, env);
|
||||
printLog({
|
||||
kind: 'debug',
|
||||
name: 'EnvironmentConfig',
|
||||
value: prettyFormat(env.config),
|
||||
});
|
||||
const ast = runWithEnvironment(func, env);
|
||||
return ast;
|
||||
}
|
||||
|
||||
@@ -147,17 +152,22 @@ export function* run(
|
||||
* Note: this is split from run() to make `config` out of scope, so that all
|
||||
* access to feature flags has to be through the Environment for consistency.
|
||||
*/
|
||||
function* runWithEnvironment(
|
||||
function runWithEnvironment(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
env: Environment,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
): CodegenFunction {
|
||||
const log = (value: CompilerPipelineValue): CompilerPipelineValue => {
|
||||
printLog(value);
|
||||
env.logger?.debugLogIRs?.(value);
|
||||
return value;
|
||||
};
|
||||
const hir = lower(func, env).unwrap();
|
||||
yield log({kind: 'hir', name: 'HIR', value: hir});
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
@@ -168,35 +178,35 @@ function* runWithEnvironment(
|
||||
!env.config.enableChangeDetectionForDebugging
|
||||
) {
|
||||
dropManualMemoization(hir);
|
||||
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InlineImmediatelyInvokedFunctionExpressions',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeConsecutiveBlocks(hir);
|
||||
yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
|
||||
enterSSA(hir);
|
||||
yield log({kind: 'hir', name: 'SSA', value: hir});
|
||||
log({kind: 'hir', name: 'SSA', value: hir});
|
||||
|
||||
eliminateRedundantPhi(hir);
|
||||
yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
|
||||
constantPropagation(hir);
|
||||
yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
|
||||
inferTypes(hir);
|
||||
yield log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir);
|
||||
@@ -214,27 +224,27 @@ function* runWithEnvironment(
|
||||
yield log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
|
||||
|
||||
analyseFunctions(hir);
|
||||
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
inferReferenceEffects(hir);
|
||||
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
|
||||
if (env.config.enableInstructionReordering) {
|
||||
instructionReordering(hir);
|
||||
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
|
||||
log({kind: 'hir', name: 'InstructionReordering', value: hir});
|
||||
}
|
||||
|
||||
pruneMaybeThrows(hir);
|
||||
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
inferMutableRanges(hir);
|
||||
yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
@@ -257,27 +267,27 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
propagatePhiTypes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagatePhiTypes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
inferReactiveScopeVariables(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
|
||||
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
value: hir,
|
||||
@@ -289,39 +299,39 @@ function* runWithEnvironment(
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
}
|
||||
|
||||
alignMethodCallScopes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignMethodCallScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
alignObjectMethodScopes(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignObjectMethodScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
value: hir,
|
||||
@@ -329,7 +339,7 @@ function* runWithEnvironment(
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
value: hir,
|
||||
@@ -338,14 +348,14 @@ function* runWithEnvironment(
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
value: hir,
|
||||
@@ -353,7 +363,7 @@ function* runWithEnvironment(
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
value: hir,
|
||||
@@ -365,7 +375,7 @@ function* runWithEnvironment(
|
||||
|
||||
if (env.config.inlineJsxTransform) {
|
||||
inlineJsxTransform(hir, env.config.inlineJsxTransform);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'inlineJsxTransform',
|
||||
value: hir,
|
||||
@@ -373,7 +383,7 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveFunction',
|
||||
value: reactiveFunction,
|
||||
@@ -382,7 +392,7 @@ function* runWithEnvironment(
|
||||
assertWellFormedBreakTargets(reactiveFunction);
|
||||
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLabels',
|
||||
value: reactiveFunction,
|
||||
@@ -390,35 +400,35 @@ function* runWithEnvironment(
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonEscapingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonReactiveDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'MergeReactiveScopesThatInvalidateTogether',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneAlwaysInvalidatingScopes',
|
||||
value: reactiveFunction,
|
||||
@@ -426,7 +436,7 @@ function* runWithEnvironment(
|
||||
|
||||
if (env.config.enableChangeDetectionForDebugging != null) {
|
||||
pruneInitializationDependencies(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneInitializationDependencies',
|
||||
value: reactiveFunction,
|
||||
@@ -434,49 +444,49 @@ function* runWithEnvironment(
|
||||
}
|
||||
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateEarlyReturns',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PromoteUsedTemporaries',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'ExtractScopeDeclarationsFromDestructuring',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'StabilizeBlockIds',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
const uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'RenameVariables',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
yield log({
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneHoistedContexts',
|
||||
value: reactiveFunction,
|
||||
@@ -497,9 +507,9 @@ function* runWithEnvironment(
|
||||
uniqueIdentifiers,
|
||||
fbtOperands,
|
||||
}).unwrap();
|
||||
yield log({kind: 'ast', name: 'Codegen', value: ast});
|
||||
log({kind: 'ast', name: 'Codegen', value: ast});
|
||||
for (const outlined of ast.outlined) {
|
||||
yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,7 +535,7 @@ export function compileFn(
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
): CodegenFunction {
|
||||
let generator = run(
|
||||
return run(
|
||||
func,
|
||||
config,
|
||||
fnType,
|
||||
@@ -534,15 +544,9 @@ export function compileFn(
|
||||
filename,
|
||||
code,
|
||||
);
|
||||
while (true) {
|
||||
const next = generator.next();
|
||||
if (next.done) {
|
||||
return next.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function log(value: CompilerPipelineValue): CompilerPipelineValue {
|
||||
function printLog(value: CompilerPipelineValue): CompilerPipelineValue {
|
||||
switch (value.kind) {
|
||||
case 'ast': {
|
||||
logCodegenFunction(value.name, value.value);
|
||||
@@ -566,14 +570,3 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function* runPlayground(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
const ast = yield* run(func, config, fnType, '_c', null, null, null);
|
||||
return ast;
|
||||
}
|
||||
|
||||
@@ -168,11 +168,19 @@ const EnvironmentConfigSchema = z.object({
|
||||
customMacros: z.nullable(z.array(MacroSchema)).default(null),
|
||||
|
||||
/**
|
||||
* Enable a check that resets the memoization cache when the source code of the file changes.
|
||||
* This is intended to support hot module reloading (HMR), where the same runtime component
|
||||
* instance will be reused across different versions of the component source.
|
||||
* Enable a check that resets the memoization cache when the source code of
|
||||
* the file changes. This is intended to support hot module reloading (HMR),
|
||||
* where the same runtime component instance will be reused across different
|
||||
* versions of the component source.
|
||||
*
|
||||
* When set to
|
||||
* - true: code for HMR support is always generated, regardless of NODE_ENV
|
||||
* or `globalThis.__DEV__`
|
||||
* - false: code for HMR support is not generated
|
||||
* - null: (default) code for HMR support is conditionally generated dependent
|
||||
* on `NODE_ENV` and `globalThis.__DEV__` at the time of compilation.
|
||||
*/
|
||||
enableResetCacheOnSourceFileChanges: z.boolean().default(false),
|
||||
enableResetCacheOnSourceFileChanges: z.nullable(z.boolean()).default(null),
|
||||
|
||||
/**
|
||||
* Enable using information from existing useMemo/useCallback to understand when a value is done
|
||||
@@ -708,7 +716,10 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
|
||||
if (
|
||||
key !== 'enableResetCacheOnSourceFileChanges' &&
|
||||
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
|
||||
) {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
}
|
||||
@@ -718,9 +729,15 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
maybeConfig[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(all)
|
||||
'use no memo';
|
||||
|
||||
function TestComponent({x}) {
|
||||
'use memo';
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode(all)
|
||||
"use no memo";
|
||||
|
||||
function TestComponent({ x }) {
|
||||
"use memo";
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// @compilationMode(all)
|
||||
'use no memo';
|
||||
|
||||
function TestComponent({x}) {
|
||||
'use memo';
|
||||
return <Button>{x}</Button>;
|
||||
}
|
||||
@@ -17,8 +17,6 @@ export {
|
||||
compileFn as compile,
|
||||
compileProgram,
|
||||
parsePluginOptions,
|
||||
run,
|
||||
runPlayground,
|
||||
OPT_OUT_DIRECTIVES,
|
||||
OPT_IN_DIRECTIVES,
|
||||
findDirectiveEnablingMemoization,
|
||||
|
||||
Reference in New Issue
Block a user