Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc50ab4bff | ||
|
|
e33071c614 | ||
|
|
c0060cf2a6 | ||
|
|
bd76b456c1 | ||
|
|
b354bbd2d2 |
@@ -35,6 +35,20 @@ yarn snap -p <file-basename> -d
|
||||
yarn snap -u
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
# Run lint on the compiler source
|
||||
yarn workspace babel-plugin-react-compiler lint
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
```bash
|
||||
# Run prettier on all files (from the react root directory, not compiler/)
|
||||
yarn prettier-all
|
||||
```
|
||||
|
||||
## Compiling Arbitrary Files
|
||||
|
||||
Use `yarn snap compile` to compile any file (not just fixtures) with the React Compiler:
|
||||
|
||||
@@ -83,6 +83,8 @@ export class ProgramContext {
|
||||
knownReferencedNames: Set<string> = new Set();
|
||||
// generated imports
|
||||
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
|
||||
// hoisted program-level constants
|
||||
constants: Map<string, {uid: string; init: t.Expression}> = new Map();
|
||||
|
||||
constructor({
|
||||
program,
|
||||
@@ -182,6 +184,16 @@ export class ProgramContext {
|
||||
this.knownReferencedNames.add(name);
|
||||
}
|
||||
|
||||
addProgramConstant(name: string, init: () => t.Expression): string {
|
||||
const existing = this.constants.get(name);
|
||||
if (existing !== undefined) {
|
||||
return existing.uid;
|
||||
}
|
||||
const uid = this.newUid(name);
|
||||
this.constants.set(name, {uid, init: init()});
|
||||
return uid;
|
||||
}
|
||||
|
||||
assertGlobalBinding(
|
||||
name: string,
|
||||
localScope?: BabelScope,
|
||||
@@ -302,6 +314,13 @@ export function addImportsToProgram(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [, {uid, init}] of programContext.constants) {
|
||||
stmts.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(t.identifier(uid), init),
|
||||
]),
|
||||
);
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
}
|
||||
|
||||
|
||||
@@ -502,6 +502,16 @@ export const EnvironmentConfigSchema = z.object({
|
||||
* 3. Force update / external sync - should use useSyncExternalStore
|
||||
*/
|
||||
enableVerboseNoSetStateInEffect: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* When enabled, the compiler emits `!Object.is(a, b)` instead of `a !== b`
|
||||
* for dependency comparisons in memoized scopes. This matches the comparison
|
||||
* semantics used by React's own hooks (useState, useMemo, etc.), which use
|
||||
* Object.is. The main difference is in handling of NaN: `NaN !== NaN` is
|
||||
* always true (causing memos to never be reused), while
|
||||
* `Object.is(NaN, NaN)` is true (correctly treating NaN as unchanged).
|
||||
*/
|
||||
enableObjectIsComparison: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
@@ -182,8 +182,8 @@ export function codegenFunction(
|
||||
const index = cx.synthesizeName('$i');
|
||||
preface.push(
|
||||
t.ifStatement(
|
||||
t.binaryExpression(
|
||||
'!==',
|
||||
codegenNegatedComparison(
|
||||
cx,
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName('$')),
|
||||
t.numericLiteral(fastRefreshState.cacheIndex),
|
||||
@@ -557,6 +557,23 @@ function codegenBlockNoReset(
|
||||
return t.blockStatement(statements);
|
||||
}
|
||||
|
||||
function codegenNegatedComparison(
|
||||
cx: Context,
|
||||
left: t.Expression,
|
||||
right: t.Expression,
|
||||
): t.Expression {
|
||||
if (cx.env.config.enableObjectIsComparison) {
|
||||
const isName = cx.env.programContext.addProgramConstant('is', () =>
|
||||
t.memberExpression(t.identifier('Object'), t.identifier('is')),
|
||||
);
|
||||
return t.unaryExpression(
|
||||
'!',
|
||||
t.callExpression(t.identifier(isName), [left, right]),
|
||||
);
|
||||
}
|
||||
return t.binaryExpression('!==', left, right);
|
||||
}
|
||||
|
||||
function codegenReactiveScope(
|
||||
cx: Context,
|
||||
statements: Array<t.Statement>,
|
||||
@@ -574,8 +591,8 @@ function codegenReactiveScope(
|
||||
|
||||
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
|
||||
const index = cx.nextCacheIndex;
|
||||
const comparison = t.binaryExpression(
|
||||
'!==',
|
||||
const comparison = codegenNegatedComparison(
|
||||
cx,
|
||||
t.memberExpression(
|
||||
t.identifier(cx.synthesizeName('$')),
|
||||
t.numericLiteral(index),
|
||||
@@ -720,8 +737,8 @@ function codegenReactiveScope(
|
||||
const name: ValidIdentifierName = earlyReturnValue.value.name.value;
|
||||
statements.push(
|
||||
t.ifStatement(
|
||||
t.binaryExpression(
|
||||
'!==',
|
||||
codegenNegatedComparison(
|
||||
cx,
|
||||
t.identifier(name),
|
||||
t.callExpression(
|
||||
t.memberExpression(t.identifier('Symbol'), t.identifier('for')),
|
||||
|
||||
@@ -487,24 +487,26 @@ function validateNoRefAccessInRenderImpl(
|
||||
*/
|
||||
if (!didError) {
|
||||
const isRefLValue = isUseRefType(instr.lvalue.identifier);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
/**
|
||||
* By default we check that function call operands are not refs,
|
||||
* ref values, or functions that can access refs.
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
if (
|
||||
isRefLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
for (const operand of eachInstructionValueOperand(
|
||||
instr.value,
|
||||
)) {
|
||||
/**
|
||||
* Allow passing refs or ref-accessing functions when:
|
||||
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
|
||||
* 2. calling hooks (independently validated for ref safety)
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
}
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
for (const operand of eachInstructionValueOperand(
|
||||
instr.value,
|
||||
)) {
|
||||
/**
|
||||
* Special case: the lvalue is passed as a jsx child
|
||||
*
|
||||
@@ -513,7 +515,98 @@ function validateNoRefAccessInRenderImpl(
|
||||
* render function which attempts to obey the rules.
|
||||
*/
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
} else {
|
||||
}
|
||||
} else if (hookKind == null && instr.effects != null) {
|
||||
/**
|
||||
* For non-hook functions with known aliasing effects, use the
|
||||
* effects to determine what validation to apply for each place.
|
||||
* Track visited id:kind pairs to avoid duplicate errors.
|
||||
*/
|
||||
const visitedEffects: Set<string> = new Set();
|
||||
for (const effect of instr.effects) {
|
||||
let place: Place | null = null;
|
||||
let validation: 'ref-passed' | 'direct-ref' | 'none' = 'none';
|
||||
switch (effect.kind) {
|
||||
case 'Freeze': {
|
||||
place = effect.value;
|
||||
validation = 'direct-ref';
|
||||
break;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
place = effect.value;
|
||||
validation = 'ref-passed';
|
||||
break;
|
||||
}
|
||||
case 'Render': {
|
||||
place = effect.place;
|
||||
validation = 'ref-passed';
|
||||
break;
|
||||
}
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
case 'MaybeAlias':
|
||||
case 'Assign':
|
||||
case 'CreateFrom': {
|
||||
place = effect.from;
|
||||
validation = 'ref-passed';
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
/**
|
||||
* ImmutableCapture can come from two sources:
|
||||
* 1. A known signature that explicitly freezes the operand
|
||||
* (e.g. PanResponder.create) — safe, the function doesn't
|
||||
* call callbacks during render.
|
||||
* 2. Downgraded defaults when the operand is already frozen
|
||||
* (e.g. foo(propRef)) — the function is unknown and may
|
||||
* access the ref.
|
||||
*
|
||||
* We distinguish these by checking whether the same operand
|
||||
* also has a Freeze effect on this instruction, which only
|
||||
* comes from known signatures.
|
||||
*/
|
||||
place = effect.from;
|
||||
const isFrozen = instr.effects.some(
|
||||
e =>
|
||||
e.kind === 'Freeze' &&
|
||||
e.value.identifier.id === effect.from.identifier.id,
|
||||
);
|
||||
validation = isFrozen ? 'direct-ref' : 'ref-passed';
|
||||
break;
|
||||
}
|
||||
case 'Create':
|
||||
case 'CreateFunction':
|
||||
case 'Apply':
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (place !== null && validation !== 'none') {
|
||||
const key = `${place.identifier.id}:${validation}`;
|
||||
if (!visitedEffects.has(key)) {
|
||||
visitedEffects.add(key);
|
||||
if (validation === 'direct-ref') {
|
||||
validateNoDirectRefValueAccess(errors, place, env);
|
||||
} else {
|
||||
validateNoRefPassedToFunction(
|
||||
errors,
|
||||
env,
|
||||
place,
|
||||
place.loc,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(
|
||||
instr.value,
|
||||
)) {
|
||||
validateNoRefPassedToFunction(
|
||||
errors,
|
||||
env,
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender:true
|
||||
import {mutate} from 'shared-runtime';
|
||||
|
||||
function Foo(props, ref) {
|
||||
console.log(ref.current);
|
||||
mutate(ref.current);
|
||||
return <div>{props.bar}</div>;
|
||||
}
|
||||
|
||||
@@ -26,14 +28,14 @@ Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.validate-mutate-ref-arg-in-render.ts:3:14
|
||||
1 | // @validateRefAccessDuringRender:true
|
||||
2 | function Foo(props, ref) {
|
||||
> 3 | console.log(ref.current);
|
||||
| ^^^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
4 | return <div>{props.bar}</div>;
|
||||
5 | }
|
||||
6 |
|
||||
error.validate-mutate-ref-arg-in-render.ts:5:9
|
||||
3 |
|
||||
4 | function Foo(props, ref) {
|
||||
> 5 | mutate(ref.current);
|
||||
| ^^^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
6 | return <div>{props.bar}</div>;
|
||||
7 | }
|
||||
8 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @validateRefAccessDuringRender:true
|
||||
import {mutate} from 'shared-runtime';
|
||||
|
||||
function Foo(props, ref) {
|
||||
console.log(ref.current);
|
||||
mutate(ref.current);
|
||||
return <div>{props.bar}</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableObjectIsComparison
|
||||
const is = null;
|
||||
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
console.log(is);
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
const _is = Object.is; // @enableObjectIsComparison
|
||||
const is = null;
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if (!_is($[0], props.x)) {
|
||||
t0 = [props.x];
|
||||
$[0] = props.x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
console.log(is);
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [42]
|
||||
[42]
|
||||
[3.14]
|
||||
logs: [null,null,null]
|
||||
@@ -0,0 +1,14 @@
|
||||
// @enableObjectIsComparison
|
||||
const is = null;
|
||||
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
console.log(is);
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableObjectIsComparison
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
let x = [];
|
||||
if (props.cond) {
|
||||
x.push(props.a);
|
||||
return x;
|
||||
} else {
|
||||
return makeArray(props.b);
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 42},
|
||||
{cond: true, a: 42},
|
||||
{cond: false, b: 3.14},
|
||||
{cond: false, b: 3.14},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
const is = Object.is; // @enableObjectIsComparison
|
||||
import { makeArray } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(6);
|
||||
let t0;
|
||||
if (!is($[0], props.a) || !is($[1], props.b) || !is($[2], props.cond)) {
|
||||
t0 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const x = [];
|
||||
if (props.cond) {
|
||||
x.push(props.a);
|
||||
t0 = x;
|
||||
break bb0;
|
||||
} else {
|
||||
let t1;
|
||||
if (!is($[4], props.b)) {
|
||||
t1 = makeArray(props.b);
|
||||
$[4] = props.b;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
t0 = t1;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
$[0] = props.a;
|
||||
$[1] = props.b;
|
||||
$[2] = props.cond;
|
||||
$[3] = t0;
|
||||
} else {
|
||||
t0 = $[3];
|
||||
}
|
||||
if (!is(t0, Symbol.for("react.early_return_sentinel"))) {
|
||||
return t0;
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
sequentialRenders: [
|
||||
{ cond: true, a: 42 },
|
||||
{ cond: true, a: 42 },
|
||||
{ cond: false, b: 3.14 },
|
||||
{ cond: false, b: 3.14 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [42]
|
||||
[42]
|
||||
[3.14]
|
||||
[3.14]
|
||||
@@ -0,0 +1,23 @@
|
||||
// @enableObjectIsComparison
|
||||
import {makeArray} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
let x = [];
|
||||
if (props.cond) {
|
||||
x.push(props.a);
|
||||
return x;
|
||||
} else {
|
||||
return makeArray(props.b);
|
||||
}
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
sequentialRenders: [
|
||||
{cond: true, a: 42},
|
||||
{cond: true, a: 42},
|
||||
{cond: false, b: 3.14},
|
||||
{cond: false, b: 3.14},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
const is = Object.is; // @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
|
||||
function Component(props) {
|
||||
const $ = _c(3);
|
||||
if (
|
||||
!is(
|
||||
$[0],
|
||||
"eb2ec56d8fdd083c203a119ff37576dc8782330598640f725524102ae79e8b5c",
|
||||
)
|
||||
) {
|
||||
for (let $i = 0; $i < 3; $i += 1) {
|
||||
$[$i] = Symbol.for("react.memo_cache_sentinel");
|
||||
}
|
||||
$[0] = "eb2ec56d8fdd083c203a119ff37576dc8782330598640f725524102ae79e8b5c";
|
||||
}
|
||||
let t0;
|
||||
if (!is($[1], props.x)) {
|
||||
t0 = [props.x];
|
||||
$[1] = props.x;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [42]
|
||||
[42]
|
||||
[3.14]
|
||||
@@ -0,0 +1,11 @@
|
||||
// @enableObjectIsComparison @enableResetCacheOnSourceFileChanges
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableObjectIsComparison
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
const is = Object.is; // @enableObjectIsComparison
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if (!is($[0], props.x)) {
|
||||
t0 = [props.x];
|
||||
$[0] = props.x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 42 }],
|
||||
sequentialRenders: [{ x: 42 }, { x: 42 }, { x: 3.14 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [42]
|
||||
[42]
|
||||
[3.14]
|
||||
@@ -0,0 +1,11 @@
|
||||
// @enableObjectIsComparison
|
||||
function Component(props) {
|
||||
const x = [props.x];
|
||||
return x;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 42}],
|
||||
sequentialRenders: [{x: 42}, {x: 42}, {x: 3.14}],
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
import {PanResponder, Stringify} from 'shared-runtime';
|
||||
|
||||
export default component Playground() {
|
||||
const onDragEndRef = useRef(() => {});
|
||||
useEffect(() => {
|
||||
onDragEndRef.current = () => {
|
||||
console.log('drag ended');
|
||||
};
|
||||
});
|
||||
const panResponder = useMemo(
|
||||
() =>
|
||||
PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
onDragEndRef.current();
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
return <Stringify responder={panResponder} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { PanResponder, Stringify } from "shared-runtime";
|
||||
|
||||
export default function Playground() {
|
||||
const $ = _c(3);
|
||||
const onDragEndRef = useRef(_temp);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
onDragEndRef.current = _temp2;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
useEffect(t0);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
onDragEndRef.current();
|
||||
},
|
||||
});
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const panResponder = t1;
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Stringify responder={panResponder} />;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp2() {
|
||||
console.log("drag ended");
|
||||
}
|
||||
function _temp() {}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import {PanResponder, Stringify} from 'shared-runtime';
|
||||
|
||||
export default component Playground() {
|
||||
const onDragEndRef = useRef(() => {});
|
||||
useEffect(() => {
|
||||
onDragEndRef.current = () => {
|
||||
console.log('drag ended');
|
||||
};
|
||||
});
|
||||
const panResponder = useMemo(
|
||||
() =>
|
||||
PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
onDragEndRef.current();
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
return <Stringify responder={panResponder} />;
|
||||
}
|
||||
@@ -196,6 +196,44 @@ export function makeSharedRuntimeTypeProvider({
|
||||
],
|
||||
},
|
||||
},
|
||||
PanResponder: {
|
||||
kind: 'object',
|
||||
properties: {
|
||||
create: {
|
||||
kind: 'function',
|
||||
positionalParams: [EffectEnum.Freeze],
|
||||
restParam: null,
|
||||
calleeEffect: EffectEnum.Read,
|
||||
returnType: {kind: 'type', name: 'Any'},
|
||||
returnValueKind: ValueKindEnum.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@config'],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@config',
|
||||
reason: ValueReasonEnum.KnownReturnSignature,
|
||||
},
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKindEnum.Frozen,
|
||||
reason: ValueReasonEnum.KnownReturnSignature,
|
||||
},
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: '@config',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (moduleName === 'ReactCompilerKnownIncompatibleTest') {
|
||||
|
||||
@@ -421,4 +421,10 @@ export function typedMutate(x: any, v: any = null): void {
|
||||
x.property = v;
|
||||
}
|
||||
|
||||
export const PanResponder = {
|
||||
create(obj: any): any {
|
||||
return obj;
|
||||
},
|
||||
};
|
||||
|
||||
export default typedLog;
|
||||
|
||||
@@ -54,6 +54,7 @@ Each filter object must include `type` and `isEnabled`. Some filters also requir
|
||||
|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| `host` | `"localhost"` | Socket connection to frontend should use this host. |
|
||||
| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
|
||||
| `path` | `""` | Path appended to the WebSocket URI (e.g. `"/__react_devtools__/"`). Useful when proxying through a reverse proxy on a subpath. A leading `/` is added automatically if missing. |
|
||||
| `port` | `8097` | Socket connection to frontend should use this port. |
|
||||
| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
|
||||
| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
|
||||
@@ -141,16 +142,51 @@ function onStatus(
|
||||
}
|
||||
```
|
||||
|
||||
#### `startServer(port?: number, host?: string, httpsOptions?: Object, loggerOptions?: Object)`
|
||||
#### `startServer(port?, host?, httpsOptions?, loggerOptions?, path?, clientOptions?)`
|
||||
Start a socket server (used to communicate between backend and frontend) and renders the DevTools UI.
|
||||
|
||||
This method accepts the following parameters:
|
||||
| Name | Default | Description |
|
||||
|---|---|---|
|
||||
| `port` | `8097` | Socket connection to backend should use this port. |
|
||||
| `host` | `"localhost"` | Socket connection to backend should use this host. |
|
||||
| `port` | `8097` | Port the local server listens on. |
|
||||
| `host` | `"localhost"` | Host the local server binds to. |
|
||||
| `httpsOptions` | | _Optional_ object defining `key` and `cert` strings. |
|
||||
| `loggerOptions` | | _Optional_ object defining a `surface` string (to be included with DevTools logging events). |
|
||||
| `path` | | _Optional_ path to append to the WebSocket URI served to connecting clients (e.g. `"/__react_devtools__/"`). Also set via the `REACT_DEVTOOLS_PATH` env var in the Electron app. |
|
||||
| `clientOptions` | | _Optional_ object with client-facing overrides (see below). |
|
||||
|
||||
##### `clientOptions`
|
||||
|
||||
When connecting through a reverse proxy, the client may need to connect to a different host, port, or protocol than the local server. Use `clientOptions` to override what appears in the `connectToDevTools()` script served to clients. Any field not set falls back to the corresponding server value.
|
||||
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `host` | server `host` | Host the client connects to. |
|
||||
| `port` | server `port` | Port the client connects to. |
|
||||
| `useHttps` | server `useHttps` | Whether the client should use `wss://`. |
|
||||
|
||||
These can also be set via environment variables in the Electron app:
|
||||
|
||||
| Env Var | Description |
|
||||
|---|---|
|
||||
| `REACT_DEVTOOLS_CLIENT_HOST` | Overrides the host in the served client script. |
|
||||
| `REACT_DEVTOOLS_CLIENT_PORT` | Overrides the port in the served client script. |
|
||||
| `REACT_DEVTOOLS_CLIENT_USE_HTTPS` | Set to `"true"` to make the served client script use `wss://`. |
|
||||
|
||||
##### Reverse proxy example
|
||||
|
||||
Run DevTools locally on the default port, but tell clients to connect through a remote proxy:
|
||||
```sh
|
||||
REACT_DEVTOOLS_CLIENT_HOST=remote.example.com \
|
||||
REACT_DEVTOOLS_CLIENT_PORT=443 \
|
||||
REACT_DEVTOOLS_CLIENT_USE_HTTPS=true \
|
||||
REACT_DEVTOOLS_PATH=/__react_devtools__/ \
|
||||
react-devtools
|
||||
```
|
||||
The server listens on `localhost:8097`. The served script tells clients:
|
||||
```js
|
||||
connectToDevTools({host: 'remote.example.com', port: 443, useHttps: true, path: '/__react_devtools__/'})
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
|
||||
5
packages/react-devtools-core/src/backend.js
vendored
5
packages/react-devtools-core/src/backend.js
vendored
@@ -33,6 +33,7 @@ import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeS
|
||||
type ConnectOptions = {
|
||||
host?: string,
|
||||
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
|
||||
path?: string,
|
||||
port?: number,
|
||||
useHttps?: boolean,
|
||||
resolveRNStyle?: ResolveNativeStyle,
|
||||
@@ -93,6 +94,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
const {
|
||||
host = 'localhost',
|
||||
nativeStyleEditorValidAttributes,
|
||||
path = '',
|
||||
useHttps = false,
|
||||
port = 8097,
|
||||
websocket,
|
||||
@@ -107,6 +109,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
} = options || {};
|
||||
|
||||
const protocol = useHttps ? 'wss' : 'ws';
|
||||
const prefixedPath = path !== '' && !path.startsWith('/') ? '/' + path : path;
|
||||
let retryTimeoutID: TimeoutID | null = null;
|
||||
|
||||
function scheduleRetry() {
|
||||
@@ -129,7 +132,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
let bridge: BackendBridge | null = null;
|
||||
|
||||
const messageListeners = [];
|
||||
const uri = protocol + '://' + host + ':' + port;
|
||||
const uri = protocol + '://' + host + ':' + port + prefixedPath;
|
||||
|
||||
// If existing websocket is passed, use it.
|
||||
// This is necessary to support our custom integrations.
|
||||
|
||||
47
packages/react-devtools-core/src/standalone.js
vendored
47
packages/react-devtools-core/src/standalone.js
vendored
@@ -306,11 +306,19 @@ type LoggerOptions = {
|
||||
surface?: ?string,
|
||||
};
|
||||
|
||||
type ClientOptions = {
|
||||
host?: string,
|
||||
port?: number,
|
||||
useHttps?: boolean,
|
||||
};
|
||||
|
||||
function startServer(
|
||||
port: number = 8097,
|
||||
host: string = 'localhost',
|
||||
httpsOptions?: ServerOptions,
|
||||
loggerOptions?: LoggerOptions,
|
||||
path?: string,
|
||||
clientOptions?: ClientOptions,
|
||||
): {close(): void} {
|
||||
registerDevToolsEventLogger(loggerOptions?.surface ?? 'standalone');
|
||||
|
||||
@@ -345,7 +353,18 @@ function startServer(
|
||||
server.on('error', (event: $FlowFixMe) => {
|
||||
onError(event);
|
||||
log.error('Failed to start the DevTools server', event);
|
||||
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
|
||||
startServerTimeoutID = setTimeout(
|
||||
() =>
|
||||
startServer(
|
||||
port,
|
||||
host,
|
||||
httpsOptions,
|
||||
loggerOptions,
|
||||
path,
|
||||
clientOptions,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
});
|
||||
|
||||
httpServer.on('request', (request: $FlowFixMe, response: $FlowFixMe) => {
|
||||
@@ -358,14 +377,21 @@ function startServer(
|
||||
// This will ensure that saved filters are shared across different web pages.
|
||||
const componentFiltersString = JSON.stringify(getSavedComponentFilters());
|
||||
|
||||
// Client overrides: when connecting through a reverse proxy, the client
|
||||
// may need to connect to a different host/port/protocol than the server.
|
||||
const clientHost = clientOptions?.host ?? host;
|
||||
const clientPort = clientOptions?.port ?? port;
|
||||
const clientUseHttps = clientOptions?.useHttps ?? useHttps;
|
||||
|
||||
response.end(
|
||||
backendFile.toString() +
|
||||
'\n;' +
|
||||
`var ReactDevToolsBackend = typeof ReactDevToolsBackend !== "undefined" ? ReactDevToolsBackend : require("ReactDevToolsBackend");\n` +
|
||||
`ReactDevToolsBackend.initialize(undefined, undefined, undefined, ${componentFiltersString});` +
|
||||
'\n' +
|
||||
`ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
|
||||
useHttps ? 'true' : 'false'
|
||||
}});
|
||||
`ReactDevToolsBackend.connectToDevTools({port: ${clientPort}, host: '${clientHost}', useHttps: ${
|
||||
clientUseHttps ? 'true' : 'false'
|
||||
}${path != null ? `, path: '${path}'` : ''}});
|
||||
`,
|
||||
);
|
||||
});
|
||||
@@ -373,7 +399,18 @@ function startServer(
|
||||
httpServer.on('error', (event: $FlowFixMe) => {
|
||||
onError(event);
|
||||
statusListener('Failed to start the server.', 'error');
|
||||
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
|
||||
startServerTimeoutID = setTimeout(
|
||||
() =>
|
||||
startServer(
|
||||
port,
|
||||
host,
|
||||
httpsOptions,
|
||||
loggerOptions,
|
||||
path,
|
||||
clientOptions,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
|
||||
@@ -44,6 +44,7 @@ module.exports = {
|
||||
// This name is important; standalone references it in order to connect.
|
||||
library: 'ReactDevToolsBackend',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -87,7 +87,31 @@ This will ensure the developer tools are connected. **Don’t forget to remove i
|
||||
|
||||
## Advanced
|
||||
|
||||
By default DevTools listen to port `8097` on `localhost`. The port can be modified by setting the `REACT_DEVTOOLS_PORT` environment variable. If you need to further customize host, port, or other settings, see the `react-devtools-core` package instead.
|
||||
By default DevTools listen to port `8097` on `localhost`. If you need to customize the server or client connection settings, the following environment variables are available:
|
||||
|
||||
| Env Var | Default | Description |
|
||||
|---|---|---|
|
||||
| `HOST` | `"localhost"` | Host the local server binds to. |
|
||||
| `PORT` | `8097` | Port the local server listens on. |
|
||||
| `REACT_DEVTOOLS_PORT` | | Alias for `PORT`. Takes precedence if both are set. |
|
||||
| `KEY` | | Path to an SSL key file. Enables HTTPS when set alongside `CERT`. |
|
||||
| `CERT` | | Path to an SSL certificate file. Enables HTTPS when set alongside `KEY`. |
|
||||
| `REACT_DEVTOOLS_PATH` | | Path appended to the WebSocket URI served to clients (e.g. `/__react_devtools__/`). |
|
||||
| `REACT_DEVTOOLS_CLIENT_HOST` | `HOST` | Overrides the host in the script served to connecting clients. |
|
||||
| `REACT_DEVTOOLS_CLIENT_PORT` | `PORT` | Overrides the port in the script served to connecting clients. |
|
||||
| `REACT_DEVTOOLS_CLIENT_USE_HTTPS` | | Set to `"true"` to make the served client script use `wss://`. |
|
||||
|
||||
When connecting through a reverse proxy, use the `REACT_DEVTOOLS_CLIENT_*` variables to tell clients to connect to a different host/port/protocol than the local server:
|
||||
|
||||
```sh
|
||||
REACT_DEVTOOLS_CLIENT_HOST=remote.example.com \
|
||||
REACT_DEVTOOLS_CLIENT_PORT=443 \
|
||||
REACT_DEVTOOLS_CLIENT_USE_HTTPS=true \
|
||||
REACT_DEVTOOLS_PATH=/__react_devtools__/ \
|
||||
react-devtools
|
||||
```
|
||||
|
||||
For more details, see the [`react-devtools-core` documentation](https://github.com/facebook/react/tree/main/packages/react-devtools-core).
|
||||
|
||||
## FAQ
|
||||
|
||||
|
||||
@@ -158,12 +158,19 @@
|
||||
<script>
|
||||
// window.api is defined in preload.js
|
||||
const {electron, readEnv, ip, getDevTools} = window.api;
|
||||
const {options, useHttps, host, protocol, port} = readEnv();
|
||||
const {options, useHttps, host, protocol, port, path, clientHost, clientPort, clientUseHttps} = readEnv();
|
||||
|
||||
const localIp = ip.address();
|
||||
const defaultPort = (port === 443 && useHttps) || (port === 80 && !useHttps);
|
||||
const server = defaultPort ? `${protocol}://${host}` : `${protocol}://${host}:${port}`;
|
||||
const serverIp = defaultPort ? `${protocol}://${localIp}` : `${protocol}://${localIp}:${port}`;
|
||||
|
||||
// Effective values for display URLs: client overrides take precedence over server values.
|
||||
const effectiveHost = clientHost != null ? clientHost : host;
|
||||
const effectivePort = clientPort != null ? clientPort : port;
|
||||
const effectiveUseHttps = clientUseHttps != null ? clientUseHttps : useHttps;
|
||||
const effectiveProtocol = effectiveUseHttps ? 'https' : 'http';
|
||||
const defaultPort = (effectivePort === 443 && effectiveUseHttps) || (effectivePort === 80 && !effectiveUseHttps);
|
||||
const pathStr = path != null ? path : '';
|
||||
const server = defaultPort ? `${effectiveProtocol}://${effectiveHost}${pathStr}` : `${effectiveProtocol}://${effectiveHost}:${effectivePort}${pathStr}`;
|
||||
const serverIp = defaultPort ? `${effectiveProtocol}://${localIp}${pathStr}` : `${effectiveProtocol}://${localIp}:${effectivePort}${pathStr}`;
|
||||
const $ = document.querySelector.bind(document);
|
||||
|
||||
let timeoutID;
|
||||
@@ -234,7 +241,7 @@
|
||||
element.innerText = status;
|
||||
}
|
||||
})
|
||||
.startServer(port, host, options);
|
||||
.startServer(port, host, options, undefined, path, {host: clientHost, port: clientPort, useHttps: clientUseHttps});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
19
packages/react-devtools/preload.js
vendored
19
packages/react-devtools/preload.js
vendored
@@ -36,6 +36,23 @@ contextBridge.exposeInMainWorld('api', {
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const protocol = useHttps ? 'https' : 'http';
|
||||
const port = +process.env.REACT_DEVTOOLS_PORT || +process.env.PORT || 8097;
|
||||
return {options, useHttps, host, protocol, port};
|
||||
const path = process.env.REACT_DEVTOOLS_PATH || undefined;
|
||||
const clientHost = process.env.REACT_DEVTOOLS_CLIENT_HOST || undefined;
|
||||
const clientPort = process.env.REACT_DEVTOOLS_CLIENT_PORT
|
||||
? +process.env.REACT_DEVTOOLS_CLIENT_PORT
|
||||
: undefined;
|
||||
const clientUseHttps =
|
||||
process.env.REACT_DEVTOOLS_CLIENT_USE_HTTPS === 'true' ? true : undefined;
|
||||
return {
|
||||
options,
|
||||
useHttps,
|
||||
host,
|
||||
protocol,
|
||||
port,
|
||||
path,
|
||||
clientHost,
|
||||
clientPort,
|
||||
clientUseHttps,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user