Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
9eb8b04a87 [compiler] Claude file/settings
Initializes CLAUDE.md and a settings file for the compiler/ directory to help use claude with the compiler. Note that some of the commands here depend on changes to snap from the next PR.
2026-01-22 14:34:47 -08:00
182 changed files with 1236 additions and 4914 deletions

View File

@@ -593,7 +593,6 @@ module.exports = {
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
Partial: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',

View File

@@ -278,7 +278,6 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node --version
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
@@ -446,7 +445,6 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
@@ -491,7 +489,6 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:

2
.gitignore vendored
View File

@@ -24,8 +24,6 @@ chrome-user-data
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {
@@ -22,7 +22,7 @@ class Component {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
class Component {
_renderMessage = () => {
const Message = () => {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
"use todo memo";
return <div>hello world!</div>;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
// @customOptOutDirectives:["use todo memo"]
function Component() {
'use todo memo';
return <div>hello world!</div>;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import { useMemo } from "react";
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
import {useMemo} from 'react';
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @gating
// @gating
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
export default 42;
@@ -12,7 +12,7 @@ export default 42;
## Code
```javascript
// @expectNothingCompiled @gating
// @gating
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
export default 42;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @gating
// @gating
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
export default 42;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Takes multiple parameters - not a component!
function Component(foo, bar) {
return <div />;

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import {useIdentity, identity} from 'shared-runtime';
function Component(fakeProps: number) {
@@ -20,7 +20,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import { useIdentity, identity } from "shared-runtime";
function Component(fakeProps: number) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
import {useIdentity, identity} from 'shared-runtime';
function Component(fakeProps: number) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const result = f(props);
function helper() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return {foo: f(props)};
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return { foo: f(props) };

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
function Component(props) {
const ignore = <foo />;
return {foo: f(props)};

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {
@@ -14,7 +14,7 @@ function Component(props) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This component is skipped bc it doesn't call any hooks or
// use JSX:
function Component(props) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in
@@ -20,7 +20,7 @@ function makeListener(instance) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Regression test for some internal code.
// This shows how the "callback rule" is more relaxed,
// and doesn't kick in unless we're confident we're in

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {
@@ -16,7 +16,7 @@ function createHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can call hooks.
function createHook() {
return function useHook() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {
@@ -15,7 +15,7 @@ function createHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because hooks can use hooks.
function createHook() {
return function useHookWithHook() {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {
@@ -15,7 +15,7 @@ function createComponentWithHook() {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// Valid because components can use hooks.
function createComponentWithHook() {
return function ComponentWithHook() {

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();
@@ -16,7 +15,6 @@ jest.useFakeTimer();
## Code
```javascript
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
// Valid because they're not matching use[A-Z].
fooState();
_use();

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {
@@ -17,7 +16,6 @@ class C {
## Code
```javascript
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
// Valid because classes can call functions.
// We don't consider these to be hooks.
class C {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {
@@ -16,7 +16,7 @@ unknownFunction(function (foo, bar) {
## Code
```javascript
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @compilationMode:"infer"
// @compilationMode:"infer"
// This is valid because "use"-prefixed functions called in
// unnamed function arguments are not assumed to be hooks.
unknownFunction(function (foo, bar) {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Invalid because it's dangerous.
@@ -22,7 +22,7 @@ useCustomHook();
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Invalid because it's dangerous.

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Invalid because it's dangerous.

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// This is a false positive (it's valid) that unfortunately
@@ -20,7 +20,7 @@ class Foo extends Component {
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// This is a false positive (it's valid) that unfortunately

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// This is a false positive (it's valid) that unfortunately

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Technically this is a false positive.
@@ -23,7 +23,7 @@ const browserHistory = useBasename(createHistory)({
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Technically this is a false positive.

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
// Technically this is a false positive.

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {
@@ -16,7 +16,7 @@
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithHook extends React.Component {
@@ -16,7 +16,7 @@ class ClassComponentWithHook extends React.Component {
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithHook extends React.Component {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithHook extends React.Component {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithFeatureFlag extends React.Component {
@@ -18,7 +18,7 @@ class ClassComponentWithFeatureFlag extends React.Component {
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithFeatureFlag extends React.Component {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class ClassComponentWithFeatureFlag extends React.Component {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {
@@ -16,7 +16,7 @@
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class C {
@@ -17,7 +17,7 @@ class C {
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class C {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
class C {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {
@@ -16,7 +16,7 @@
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -2,7 +2,7 @@
## Input
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {
@@ -16,7 +16,7 @@
## Code
```javascript
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -1,4 +1,4 @@
// @expectNothingCompiled @skip
// @skip
// Passed but should have failed
(class {

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
import {c as useMemoCache} from 'react/compiler-runtime';
function Component(props) {
@@ -27,7 +26,6 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled
import { c as useMemoCache } from "react/compiler-runtime";
function Component(props) {

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
import {c as useMemoCache} from 'react/compiler-runtime';
function Component(props) {

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
function Component() {
'use no forget';
return <div>Hello World</div>;
@@ -19,7 +18,6 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled
function Component() {
"use no forget";
return <div>Hello World</div>;

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
function Component() {
'use no forget';
return <div>Hello World</div>;

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @expectNothingCompiled
function Component(props) {
'use no memo';
let x = [props.foo];
@@ -20,7 +19,6 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
// @expectNothingCompiled
function Component(props) {
"use no memo";
let x = [props.foo];

View File

@@ -1,4 +1,3 @@
// @expectNothingCompiled
function Component(props) {
'use no memo';
let x = [props.foo];

View File

@@ -52,11 +52,7 @@ function makePluginOptions(
EffectEnum: typeof Effect,
ValueKindEnum: typeof ValueKind,
ValueReasonEnum: typeof ValueReason,
): {
options: PluginOptions;
loggerTestOnly: boolean;
logs: Array<{filename: string | null; event: LoggerEvent}>;
} {
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
let validatePreserveExistingMemoizationGuarantees = false;
let target: CompilerReactTarget = '19';
@@ -73,12 +69,13 @@ function makePluginOptions(
validatePreserveExistingMemoizationGuarantees = true;
}
const loggerTestOnly = firstLine.includes('@loggerTestOnly');
const logs: Array<{filename: string | null; event: LoggerEvent}> = [];
const logger: Logger = {
logEvent: (filename, event) => {
logs.push({filename, event});
},
logEvent: firstLine.includes('@loggerTestOnly')
? (filename, event) => {
logs.push({filename, event});
}
: () => {},
debugLogIRs: debugIRLogger,
};
@@ -99,7 +96,7 @@ function makePluginOptions(
enableReanimatedCheck: false,
target,
};
return {options, loggerTestOnly, logs};
return [options, logs];
}
export function parseInput(
@@ -248,7 +245,7 @@ export async function transformFixtureInput(
/**
* Get Forget compiled code
*/
const {options, loggerTestOnly, logs} = makePluginOptions(
const [options, logs] = makePluginOptions(
firstLine,
parseConfigPragmaFn,
debugIRLogger,
@@ -345,7 +342,7 @@ export async function transformFixtureInput(
}
const forgetOutput = await format(forgetCode, language);
let formattedLogs = null;
if (loggerTestOnly && logs.length !== 0) {
if (logs.length !== 0) {
formattedLogs = logs
.map(({event}) => {
return JSON.stringify(event, (key, value) => {
@@ -361,23 +358,6 @@ export async function transformFixtureInput(
})
.join('\n');
}
const expectNothingCompiled =
firstLine.indexOf('@expectNothingCompiled') !== -1;
const successFailures = logs.filter(
log =>
log.event.kind === 'CompileSuccess' || log.event.kind === 'CompileError',
);
if (successFailures.length === 0 && !expectNothingCompiled) {
return {
kind: 'err',
msg: 'No success/failure events, add `// @expectNothingCompiled` to the first line if this is expected',
};
} else if (successFailures.length !== 0 && expectNothingCompiled) {
return {
kind: 'err',
msg: 'Expected nothing to be compiled (from `// @expectNothingCompiled`), but some functions compiled or errored',
};
}
return {
kind: 'ok',
value: {

View File

@@ -26,3 +26,5 @@ export const FIXTURES_PATH = path.join(
'compiler',
);
export const SNAPSHOT_EXTENSION = '.expect.md';
export const FILTER_FILENAME = 'testfilter.txt';
export const FILTER_PATH = path.join(PROJECT_ROOT, FILTER_FILENAME);

View File

@@ -8,7 +8,7 @@
import fs from 'fs/promises';
import * as glob from 'glob';
import path from 'path';
import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
import {FILTER_PATH, FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
const INPUT_EXTENSIONS = [
'.js',
@@ -22,9 +22,19 @@ const INPUT_EXTENSIONS = [
];
export type TestFilter = {
debug: boolean;
paths: Array<string>;
};
async function exists(file: string): Promise<boolean> {
try {
await fs.access(file);
return true;
} catch {
return false;
}
}
function stripExtension(filename: string, extensions: Array<string>): string {
for (const ext of extensions) {
if (filename.endsWith(ext)) {
@@ -34,6 +44,37 @@ function stripExtension(filename: string, extensions: Array<string>): string {
return filename;
}
export async function readTestFilter(): Promise<TestFilter | null> {
if (!(await exists(FILTER_PATH))) {
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
}
const input = await fs.readFile(FILTER_PATH, 'utf8');
const lines = input.trim().split('\n');
let debug: boolean = false;
const line0 = lines[0];
if (line0 != null) {
// Try to parse pragmas
let consumedLine0 = false;
if (line0.indexOf('@only') !== -1) {
consumedLine0 = true;
}
if (line0.indexOf('@debug') !== -1) {
debug = true;
consumedLine0 = true;
}
if (consumedLine0) {
lines.shift();
}
}
return {
debug,
paths: lines.filter(line => !line.trimStart().startsWith('//')),
};
}
export function getBasename(fixture: TestFixture): string {
return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
}

View File

@@ -8,8 +8,8 @@
import watcher from '@parcel/watcher';
import path from 'path';
import ts from 'typescript';
import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, getFixtures} from './fixture-utils';
import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, readTestFilter} from './fixture-utils';
import {execSync} from 'child_process';
export function watchSrc(
@@ -117,16 +117,6 @@ export type RunnerState = {
lastUpdate: number;
mode: RunnerMode;
filter: TestFilter | null;
debug: boolean;
// Input mode for interactive pattern entry
inputMode: 'none' | 'pattern';
inputBuffer: string;
// Autocomplete state
allFixtureNames: Array<string>;
matchingFixtures: Array<string>;
selectedIndex: number;
// Track last run status of each fixture (for autocomplete suggestions)
fixtureLastRunStatus: Map<string, 'pass' | 'fail'>;
};
function subscribeFixtures(
@@ -152,6 +142,26 @@ function subscribeFixtures(
});
}
function subscribeFilterFile(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
watcher.subscribe(PROJECT_ROOT, async (err, events) => {
if (err) {
console.error(err);
process.exit(1);
} else if (
events.findIndex(event => event.path.includes(FILTER_FILENAME)) !== -1
) {
if (state.mode.filter) {
state.filter = await readTestFilter();
state.mode.action = RunnerAction.Test;
onChange(state);
}
}
});
}
function subscribeTsc(
state: RunnerState,
onChange: (state: RunnerState) => void,
@@ -185,226 +195,20 @@ function subscribeTsc(
);
}
/**
* Levenshtein edit distance between two strings
*/
function editDistance(a: string, b: string): number {
const m = a.length;
const n = b.length;
// Create a 2D array for memoization
const dp: number[][] = Array.from({length: m + 1}, () =>
Array(n + 1).fill(0),
);
// Base cases
for (let i = 0; i <= m; i++) dp[i][0] = i;
for (let j = 0; j <= n; j++) dp[0][j] = j;
// Fill in the rest
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[m][n];
}
function filterFixtures(
allNames: Array<string>,
pattern: string,
): Array<string> {
if (pattern === '') {
return allNames;
}
const lowerPattern = pattern.toLowerCase();
const matches = allNames.filter(name =>
name.toLowerCase().includes(lowerPattern),
);
// Sort by edit distance (lower = better match)
matches.sort((a, b) => {
const distA = editDistance(lowerPattern, a.toLowerCase());
const distB = editDistance(lowerPattern, b.toLowerCase());
return distA - distB;
});
return matches;
}
const MAX_DISPLAY = 15;
function renderAutocomplete(state: RunnerState): void {
// Clear terminal
console.log('\u001Bc');
// Show current input
console.log(`Pattern: ${state.inputBuffer}`);
console.log('');
// Get current filter pattern if active
const currentFilterPattern =
state.mode.filter && state.filter ? state.filter.paths[0] : null;
// Show matching fixtures (limit to MAX_DISPLAY)
const toShow = state.matchingFixtures.slice(0, MAX_DISPLAY);
toShow.forEach((name, i) => {
const isSelected = i === state.selectedIndex;
const matchesCurrentFilter =
currentFilterPattern != null &&
name.toLowerCase().includes(currentFilterPattern.toLowerCase());
let prefix: string;
if (isSelected) {
prefix = '> ';
} else if (matchesCurrentFilter) {
prefix = '* ';
} else {
prefix = ' ';
}
console.log(`${prefix}${name}`);
});
if (state.matchingFixtures.length > MAX_DISPLAY) {
console.log(
` ... and ${state.matchingFixtures.length - MAX_DISPLAY} more`,
);
}
console.log('');
console.log('↑/↓/Tab navigate | Enter select | Esc cancel');
}
function subscribeKeyEvents(
state: RunnerState,
onChange: (state: RunnerState) => void,
) {
process.stdin.on('keypress', async (str, key) => {
// Handle input mode (pattern entry with autocomplete)
if (state.inputMode !== 'none') {
if (key.name === 'return') {
// Enter pressed - use selected fixture or typed text
let pattern: string;
if (
state.selectedIndex >= 0 &&
state.selectedIndex < state.matchingFixtures.length
) {
pattern = state.matchingFixtures[state.selectedIndex];
} else {
pattern = state.inputBuffer.trim();
}
state.inputMode = 'none';
state.inputBuffer = '';
state.allFixtureNames = [];
state.matchingFixtures = [];
state.selectedIndex = -1;
if (pattern !== '') {
state.filter = {paths: [pattern]};
state.mode.filter = true;
state.mode.action = RunnerAction.Test;
onChange(state);
}
return;
} else if (key.name === 'escape') {
// Cancel input mode
state.inputMode = 'none';
state.inputBuffer = '';
state.allFixtureNames = [];
state.matchingFixtures = [];
state.selectedIndex = -1;
// Redraw normal UI
onChange(state);
return;
} else if (key.name === 'up' || (key.name === 'tab' && key.shift)) {
// Navigate up in autocomplete list
if (state.matchingFixtures.length > 0) {
if (state.selectedIndex <= 0) {
state.selectedIndex =
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
} else {
state.selectedIndex--;
}
renderAutocomplete(state);
}
return;
} else if (key.name === 'down' || (key.name === 'tab' && !key.shift)) {
// Navigate down in autocomplete list
if (state.matchingFixtures.length > 0) {
const maxIndex =
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
if (state.selectedIndex >= maxIndex) {
state.selectedIndex = 0;
} else {
state.selectedIndex++;
}
renderAutocomplete(state);
}
return;
} else if (key.name === 'backspace') {
if (state.inputBuffer.length > 0) {
state.inputBuffer = state.inputBuffer.slice(0, -1);
state.matchingFixtures = filterFixtures(
state.allFixtureNames,
state.inputBuffer,
);
state.selectedIndex = -1;
renderAutocomplete(state);
}
return;
} else if (str && !key.ctrl && !key.meta) {
// Regular character - accumulate, filter, and render
state.inputBuffer += str;
state.matchingFixtures = filterFixtures(
state.allFixtureNames,
state.inputBuffer,
);
state.selectedIndex = -1;
renderAutocomplete(state);
return;
}
return; // Ignore other keys in input mode
}
// Normal mode keypress handling
if (key.name === 'u') {
// u => update fixtures
state.mode.action = RunnerAction.Update;
} else if (key.name === 'q') {
process.exit(0);
} else if (key.name === 'a') {
// a => exit filter mode and run all tests
state.mode.filter = false;
state.filter = null;
} else if (key.name === 'f') {
state.mode.filter = !state.mode.filter;
state.filter = state.mode.filter ? await readTestFilter() : null;
state.mode.action = RunnerAction.Test;
} else if (key.name === 'd') {
// d => toggle debug logging
state.debug = !state.debug;
state.mode.action = RunnerAction.Test;
} else if (key.name === 'p') {
// p => enter pattern input mode with autocomplete
state.inputMode = 'pattern';
state.inputBuffer = '';
// Load all fixtures for autocomplete
const fixtures = await getFixtures(null);
state.allFixtureNames = Array.from(fixtures.keys()).sort();
// Show failed fixtures first when no pattern entered
const failedFixtures = Array.from(state.fixtureLastRunStatus.entries())
.filter(([_, status]) => status === 'fail')
.map(([name]) => name)
.sort();
state.matchingFixtures =
failedFixtures.length > 0 ? failedFixtures : state.allFixtureNames;
state.selectedIndex = -1;
renderAutocomplete(state);
return; // Don't trigger onChange yet
} else {
// any other key re-runs tests
state.mode.action = RunnerAction.Test;
@@ -415,37 +219,21 @@ function subscribeKeyEvents(
export async function makeWatchRunner(
onChange: (state: RunnerState) => void,
debugMode: boolean,
initialPattern?: string,
filterMode: boolean,
): Promise<void> {
// Determine initial filter state
let filter: TestFilter | null = null;
let filterEnabled = false;
if (initialPattern) {
filter = {paths: [initialPattern]};
filterEnabled = true;
}
const state: RunnerState = {
const state = {
compilerVersion: 0,
isCompilerBuildValid: false,
lastUpdate: -1,
mode: {
action: RunnerAction.Test,
filter: filterEnabled,
filter: filterMode,
},
filter,
debug: debugMode,
inputMode: 'none',
inputBuffer: '',
allFixtureNames: [],
matchingFixtures: [],
selectedIndex: -1,
fixtureLastRunStatus: new Map(),
filter: filterMode ? await readTestFilter() : null,
};
subscribeTsc(state, onChange);
subscribeFixtures(state, onChange);
subscribeKeyEvents(state, onChange);
subscribeFilterFile(state, onChange);
}

View File

@@ -12,8 +12,8 @@ import * as readline from 'readline';
import ts from 'typescript';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {PROJECT_ROOT} from './constants';
import {TestFilter, getFixtures} from './fixture-utils';
import {FILTER_PATH, PROJECT_ROOT} from './constants';
import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
import {TestResult, TestResults, report, update} from './reporter';
import {
RunnerAction,
@@ -33,9 +33,9 @@ type RunnerOptions = {
sync: boolean;
workerThreads: boolean;
watch: boolean;
filter: boolean;
update: boolean;
pattern?: string;
debug: boolean;
};
const opts: RunnerOptions = yargs
@@ -59,16 +59,18 @@ const opts: RunnerOptions = yargs
.alias('u', 'update')
.describe('update', 'Update fixtures')
.default('update', false)
.boolean('filter')
.describe(
'filter',
'Only run fixtures which match the contents of testfilter.txt',
)
.default('filter', false)
.string('pattern')
.alias('p', 'pattern')
.describe(
'pattern',
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
)
.boolean('debug')
.alias('d', 'debug')
.describe('debug', 'Enable debug logging to print HIR for each pass')
.default('debug', false)
.help('help')
.strict()
.parseSync(hideBin(process.argv)) as RunnerOptions;
@@ -80,15 +82,12 @@ async function runFixtures(
worker: Worker & typeof runnerWorker,
filter: TestFilter | null,
compilerVersion: number,
debug: boolean,
requireSingleFixture: boolean,
): Promise<TestResults> {
// We could in theory be fancy about tracking the contents of the fixtures
// directory via our file subscription, but it's simpler to just re-read
// the directory each time.
const fixtures = await getFixtures(filter);
const isOnlyFixture = filter !== null && fixtures.size === 1;
const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
let entries: Array<[string, TestResult]>;
if (!opts.sync) {
@@ -97,7 +96,12 @@ async function runFixtures(
for (const [fixtureName, fixture] of fixtures) {
work.push(
worker
.transformFixture(fixture, compilerVersion, shouldLog, true)
.transformFixture(
fixture,
compilerVersion,
(filter?.debug ?? false) && isOnlyFixture,
true,
)
.then(result => [fixtureName, result]),
);
}
@@ -109,7 +113,7 @@ async function runFixtures(
let output = await runnerWorker.transformFixture(
fixture,
compilerVersion,
shouldLog,
(filter?.debug ?? false) && isOnlyFixture,
true,
);
entries.push([fixtureName, output]);
@@ -124,7 +128,7 @@ async function onChange(
worker: Worker & typeof runnerWorker,
state: RunnerState,
) {
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
if (isCompilerBuildValid) {
const start = performance.now();
@@ -138,18 +142,8 @@ async function onChange(
worker,
mode.filter ? filter : null,
compilerVersion,
debug,
true, // requireSingleFixture in watch mode
);
const end = performance.now();
// Track fixture status for autocomplete suggestions
for (const [basename, result] of results) {
const failed =
result.actual !== result.expected || result.unexpectedError != null;
state.fixtureLastRunStatus.set(basename, failed ? 'fail' : 'pass');
}
if (mode.action === RunnerAction.Update) {
update(results);
state.lastUpdate = end;
@@ -165,13 +159,11 @@ async function onChange(
console.log(
'\n' +
(mode.filter
? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`
? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
: 'Current mode = NORMAL, run all test fixtures.') +
'\nWaiting for input or file changes...\n' +
'u - update all fixtures\n' +
`d - toggle (turn ${debug ? 'off' : 'on'}) debug logging\n` +
'p - enter pattern to filter fixtures\n' +
(mode.filter ? 'a - run all tests (exit filter mode)\n' : '') +
`f - toggle (turn ${mode.filter ? 'off' : 'on'}) filter mode\n` +
'q - quit\n' +
'[any] - rerun tests\n',
);
@@ -188,12 +180,15 @@ export async function main(opts: RunnerOptions): Promise<void> {
worker.getStderr().pipe(process.stderr);
worker.getStdout().pipe(process.stdout);
// Check if watch mode should be enabled
const shouldWatch = opts.watch;
// If pattern is provided, force watch mode off and use pattern filter
const shouldWatch = opts.watch && opts.pattern == null;
if (opts.watch && opts.pattern != null) {
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
}
if (shouldWatch) {
makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
if (opts.pattern) {
makeWatchRunner(state => onChange(worker, state), opts.filter);
if (opts.filter) {
/**
* Warm up wormers when in watch mode. Loading the Forget babel plugin
* and all of its transitive dependencies takes 1-3s (per worker) on a M1.
@@ -241,17 +236,14 @@ export async function main(opts: RunnerOptions): Promise<void> {
let testFilter: TestFilter | null = null;
if (opts.pattern) {
testFilter = {
debug: true,
paths: [opts.pattern],
};
} else if (opts.filter) {
testFilter = await readTestFilter();
}
const results = await runFixtures(
worker,
testFilter,
0,
opts.debug,
false, // no requireSingleFixture in non-watch mode
);
const results = await runFixtures(worker, testFilter, 0);
if (opts.update) {
update(results);
isSuccess = true;

View File

@@ -1,3 +1,12 @@
.roboto-font {
font-family: "Roboto", serif;
font-optical-sizing: auto;
font-weight: 100;
font-style: normal;
font-variation-settings:
"wdth" 100;
}
.swipe-recognizer {
width: 300px;
background: #eee;

View File

@@ -4,7 +4,6 @@ import React, {
Activity,
useLayoutEffect,
useEffect,
useInsertionEffect,
useState,
useId,
useOptimistic,
@@ -42,26 +41,6 @@ const b = (
);
function Component() {
// Test inserting fonts with style tags using useInsertionEffect. This is not recommended but
// used to test that gestures etc works with useInsertionEffect so that stylesheet based
// libraries can be properly supported.
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = `
.roboto-font {
font-family: "Roboto", serif;
font-optical-sizing: auto;
font-weight: 100;
font-style: normal;
font-variation-settings:
"wdth" 100;
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
return (
<ViewTransition
default={
@@ -103,59 +82,8 @@ export default function Page({url, navigate}) {
{rotate: '0deg', transformOrigin: '30px 8px'},
{rotate: '360deg', transformOrigin: '30px 8px'},
];
const animation1 = viewTransition.old.animate(keyframes, 250);
const animation2 = viewTransition.new.animate(keyframes, 250);
return () => {
animation1.cancel();
animation2.cancel();
};
}
function onGestureTransition(
timeline,
{rangeStart, rangeEnd},
viewTransition,
types
) {
const keyframes = [
{rotate: '0deg', transformOrigin: '30px 8px'},
{rotate: '360deg', transformOrigin: '30px 8px'},
];
const reverse = rangeStart > rangeEnd;
if (timeline instanceof AnimationTimeline) {
// Native Timeline
const options = {
timeline: timeline,
direction: reverse ? 'normal' : 'reverse',
rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
};
const animation1 = viewTransition.old.animate(keyframes, options);
const animation2 = viewTransition.new.animate(keyframes, options);
return () => {
animation1.cancel();
animation2.cancel();
};
} else {
// Custom Timeline
const options = {
direction: reverse ? 'normal' : 'reverse',
// We set the delay and duration to represent the span of the range.
delay: reverse ? rangeEnd : rangeStart,
duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart,
};
const animation1 = viewTransition.old.animate(keyframes, options);
const animation2 = viewTransition.new.animate(keyframes, options);
// Let the custom timeline take control of driving the animations.
const cleanup1 = timeline.animate(animation1);
const cleanup2 = timeline.animate(animation2);
return () => {
animation1.cancel();
animation2.cancel();
cleanup1();
cleanup2();
};
}
viewTransition.old.animate(keyframes, 250);
viewTransition.new.animate(keyframes, 250);
}
function swipeAction() {
@@ -203,10 +131,7 @@ export default function Page({url, navigate}) {
);
const exclamation = (
<ViewTransition
name="exclamation"
onShare={onTransition}
onGestureShare={onGestureTransition}>
<ViewTransition name="exclamation" onShare={onTransition}>
<span>
<div>!</div>
</span>
@@ -246,20 +171,17 @@ export default function Page({url, navigate}) {
}}>
<h1>{!show ? 'A' + counter : 'B'}</h1>
</ViewTransition>
{
// Using url instead of renderedUrl here lets us only update this on commit.
url === '/?b' ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)
}
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
<ViewTransition>
{show ? (
<div>hello{exclamation}</div>

View File

@@ -114,17 +114,16 @@ export default function SwipeRecognizer({
);
}
function onGestureEnd(changed) {
// We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit
// before scheduling new updates.
// Reset scroll
if (changed) {
// Trigger side-effects
startTransition(action);
}
if (activeGesture.current !== null) {
const cancelGesture = activeGesture.current;
activeGesture.current = null;
cancelGesture();
}
if (changed) {
// Trigger side-effects
startTransition(action);
}
}
function onScrollEnd() {
if (touchTimeline.current) {

View File

@@ -826,7 +826,7 @@ declare class WebSocket extends EventTarget {
bufferedAmount: number;
extensions: string;
onopen: (ev: any) => mixed;
onmessage: (ev: MessageEvent<>) => mixed;
onmessage: (ev: MessageEvent) => mixed;
onclose: (ev: CloseEvent) => mixed;
onerror: (ev: any) => mixed;
binaryType: 'blob' | 'arraybuffer';
@@ -855,8 +855,8 @@ declare class Worker extends EventTarget {
workerOptions?: WorkerOptions
): void;
onerror: null | ((ev: any) => mixed);
onmessage: null | ((ev: MessageEvent<>) => mixed);
onmessageerror: null | ((ev: MessageEvent<>) => mixed);
onmessage: null | ((ev: MessageEvent) => mixed);
onmessageerror: null | ((ev: MessageEvent) => mixed);
postMessage(message: any, ports?: any): void;
terminate(): void;
}
@@ -888,14 +888,14 @@ declare class WorkerGlobalScope extends EventTarget {
}
declare class DedicatedWorkerGlobalScope extends WorkerGlobalScope {
onmessage: (ev: MessageEvent<>) => mixed;
onmessageerror: (ev: MessageEvent<>) => mixed;
onmessage: (ev: MessageEvent) => mixed;
onmessageerror: (ev: MessageEvent) => mixed;
postMessage(message: any, transfer?: Iterable<any>): void;
}
declare class SharedWorkerGlobalScope extends WorkerGlobalScope {
name: string;
onconnect: (ev: MessageEvent<>) => mixed;
onconnect: (ev: MessageEvent) => mixed;
}
declare class WorkerLocation {
@@ -2056,8 +2056,8 @@ declare class MessagePort extends EventTarget {
start(): void;
close(): void;
onmessage: null | ((ev: MessageEvent<>) => mixed);
onmessageerror: null | ((ev: MessageEvent<>) => mixed);
onmessage: null | ((ev: MessageEvent) => mixed);
onmessageerror: null | ((ev: MessageEvent) => mixed);
}
declare class MessageChannel {

View File

@@ -151,7 +151,7 @@ type TransitionEventHandler = (event: TransitionEvent) => mixed;
type TransitionEventListener =
| {handleEvent: TransitionEventHandler, ...}
| TransitionEventHandler;
type MessageEventHandler = (event: MessageEvent<>) => mixed;
type MessageEventHandler = (event: MessageEvent) => mixed;
type MessageEventListener =
| {handleEvent: MessageEventHandler, ...}
| MessageEventHandler;
@@ -845,8 +845,8 @@ declare class PageTransitionEvent extends Event {
// https://www.w3.org/TR/2008/WD-html5-20080610/comms.html
// and
// https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces
declare class MessageEvent<Data = mixed> extends Event {
data: Data;
declare class MessageEvent extends Event {
data: mixed;
origin: string;
lastEventId: string;
source: WindowProxy;

View File

@@ -109,8 +109,8 @@ declare class ErrorEvent extends Event {
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
declare class BroadcastChannel extends EventTarget {
name: string;
onmessage: ?(event: MessageEvent<>) => void;
onmessageerror: ?(event: MessageEvent<>) => void;
onmessage: ?(event: MessageEvent) => void;
onmessageerror: ?(event: MessageEvent) => void;
constructor(name: string): void;
postMessage(msg: mixed): void;

View File

@@ -88,7 +88,6 @@
"jest-cli": "^29.4.2",
"jest-diff": "^29.4.2",
"jest-environment-jsdom": "^29.4.2",
"jest-silent-reporter": "^0.6.0",
"jest-snapshot-serializer-raw": "^1.2.0",
"minimatch": "^3.0.4",
"minimist": "^1.2.3",

View File

@@ -1,147 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {RuleTester} from 'eslint';
import {allRules} from '../src/shared/ReactCompiler';
const ESLintTesterV8 = require('eslint-v8').RuleTester;
/**
* A string template tag that removes padding from the left side of multi-line strings
* @param {Array} strings array of code strings (only one expected)
*/
function normalizeIndent(strings: TemplateStringsArray): string {
const codeLines = strings[0]?.split('\n') ?? [];
const leftPadding = codeLines[1]?.match(/\s+/)![0] ?? '';
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
type CompilerTestCases = {
valid: RuleTester.ValidTestCase[];
invalid: RuleTester.InvalidTestCase[];
};
const tests: CompilerTestCases = {
valid: [
// ===========================================
// Tests for mayContainReactCode heuristic with Flow syntax
// Files that should be SKIPPED (no React-like function names)
// These contain code that WOULD trigger errors if compiled,
// but since the heuristic skips them, no errors are reported.
// ===========================================
{
name: '[Heuristic/Flow] Skips files with only lowercase utility functions',
filename: 'utils.js',
code: normalizeIndent`
function helper(obj) {
obj.key = 'value';
return obj;
}
`,
},
{
name: '[Heuristic/Flow] Skips lowercase arrow functions even with mutations',
filename: 'helpers.js',
code: normalizeIndent`
const processData = (input) => {
input.modified = true;
return input;
};
`,
},
],
invalid: [
// ===========================================
// Tests for mayContainReactCode heuristic with Flow component/hook syntax
// These use Flow's component/hook declarations which should be detected
// ===========================================
{
name: '[Heuristic/Flow] Compiles Flow component declaration - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
component MyComponent(a: {key: string}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles exported Flow component declaration - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
export component MyComponent(a: {key: string}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles default exported Flow component declaration - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
export default component MyComponent(a: {key: string}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles Flow hook declaration - detects argument mutation',
filename: 'hooks.js',
code: normalizeIndent`
hook useMyHook(a: {key: string}) {
a.key = 'value';
return a;
}
`,
errors: [
{
message: /Modifying component props or hook arguments/,
},
],
},
{
name: '[Heuristic/Flow] Compiles exported Flow hook declaration - detects argument mutation',
filename: 'hooks.js',
code: normalizeIndent`
export hook useMyHook(a: {key: string}) {
a.key = 'value';
return a;
}
`,
errors: [
{
message: /Modifying component props or hook arguments/,
},
],
},
],
};
const eslintTester = new ESLintTesterV8({
parser: require.resolve('hermes-eslint'),
parserOptions: {
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
});
eslintTester.run('react-compiler', allRules['immutability'].rule, tests);

View File

@@ -46,35 +46,6 @@ const tests: CompilerTestCases = {
}
`,
},
// ===========================================
// Tests for mayContainReactCode heuristic
// Files that should be SKIPPED (no React-like function names)
// These contain code that WOULD trigger errors if compiled,
// but since the heuristic skips them, no errors are reported.
// ===========================================
{
name: '[Heuristic] Skips files with only lowercase utility functions',
filename: 'utils.ts',
// This mutates an argument, which would be flagged in a component/hook,
// but this file is skipped because there are no React-like function names
code: normalizeIndent`
function helper(obj) {
obj.key = 'value';
return obj;
}
`,
},
{
name: '[Heuristic] Skips lowercase arrow functions even with mutations',
filename: 'helpers.ts',
// Would be flagged if compiled, but skipped due to lowercase name
code: normalizeIndent`
const processData = (input) => {
input.modified = true;
return input;
};
`,
},
],
invalid: [
{
@@ -97,101 +68,6 @@ const tests: CompilerTestCases = {
},
],
},
// ===========================================
// Tests for mayContainReactCode heuristic
// Files that SHOULD be compiled (have React-like function names)
// These contain violations to prove compilation happens.
// ===========================================
{
name: '[Heuristic] Compiles PascalCase function declaration - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
function MyComponent({a}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles PascalCase arrow function - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = ({a}) => {
a.key = 'value';
return <div />;
};
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles PascalCase function expression - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = function({a}) {
a.key = 'value';
return <div />;
};
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles exported function declaration - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
export function MyComponent({a}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles exported arrow function - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
export const MyComponent = ({a}) => {
a.key = 'value';
return <div />;
};
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles default exported function - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
export default function MyComponent({a}) {
a.key = 'value';
return <div />;
}
`,
errors: [
{
message: /Modifying component props/,
},
],
},
],
};

View File

@@ -5,8 +5,4 @@ process.env.NODE_ENV = 'development';
module.exports = {
setupFiles: [require.resolve('../../scripts/jest/setupEnvironment.js')],
moduleFileExtensions: ['ts', 'js', 'json'],
moduleNameMapper: {
'^babel-plugin-react-compiler$':
'<rootDir>/../../compiler/packages/babel-plugin-react-compiler/dist/index.js',
},
};

View File

@@ -17,107 +17,10 @@ import BabelPluginReactCompiler, {
LoggerEvent,
} from 'babel-plugin-react-compiler';
import type {SourceCode} from 'eslint';
import type * as ESTree from 'estree';
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
// Pattern for component names: starts with uppercase letter
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
// Pattern for hook names: starts with 'use' followed by uppercase letter or digit
const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
/**
* Quick heuristic using ESLint's already-parsed AST to detect if the file
* may contain React components or hooks based on function naming patterns.
* Only checks top-level declarations since components/hooks are declared at module scope.
* Returns true if compilation should proceed, false to skip.
*/
function mayContainReactCode(sourceCode: SourceCode): boolean {
const ast = sourceCode.ast;
// Only check top-level statements - components/hooks are declared at module scope
for (const node of ast.body) {
if (checkTopLevelNode(node)) {
return true;
}
}
return false;
}
function checkTopLevelNode(node: ESTree.Node): boolean {
// Handle Flow component/hook declarations (hermes-eslint produces these node types)
// @ts-expect-error not part of ESTree spec
if (node.type === 'ComponentDeclaration' || node.type === 'HookDeclaration') {
return true;
}
// Handle: export function MyComponent() {} or export const useHook = () => {}
if (node.type === 'ExportNamedDeclaration') {
const decl = (node as ESTree.ExportNamedDeclaration).declaration;
if (decl != null) {
return checkTopLevelNode(decl);
}
return false;
}
// Handle: export default function MyComponent() {} or export default () => {}
if (node.type === 'ExportDefaultDeclaration') {
const decl = (node as ESTree.ExportDefaultDeclaration).declaration;
// Anonymous default function export - compile conservatively
if (
decl.type === 'FunctionExpression' ||
decl.type === 'ArrowFunctionExpression' ||
(decl.type === 'FunctionDeclaration' &&
(decl as ESTree.FunctionDeclaration).id == null)
) {
return true;
}
return checkTopLevelNode(decl as ESTree.Node);
}
// Handle: function MyComponent() {}
// Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
if (node.type === 'FunctionDeclaration') {
// Check for Hermes-added flags indicating Flow component/hook syntax
if (
'__componentDeclaration' in node ||
'__hookDeclaration' in node
) {
return true;
}
const id = (node as ESTree.FunctionDeclaration).id;
if (id != null) {
const name = id.name;
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
return true;
}
}
}
// Handle: const MyComponent = () => {} or const useHook = function() {}
if (node.type === 'VariableDeclaration') {
for (const decl of (node as ESTree.VariableDeclaration).declarations) {
if (decl.id.type === 'Identifier') {
const init = decl.init;
if (
init != null &&
(init.type === 'ArrowFunctionExpression' ||
init.type === 'FunctionExpression')
) {
const name = decl.id.name;
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
return true;
}
}
}
}
}
return false;
}
const COMPILER_OPTIONS: PluginOptions = {
outputMode: 'lint',
panicThreshold: 'none',
@@ -313,24 +216,6 @@ export default function runReactCompiler({
return entry;
}
// Quick heuristic: skip files that don't appear to contain React code.
// We still cache the empty result so subsequent rules don't re-run the check.
if (!mayContainReactCode(sourceCode)) {
const emptyResult: RunCacheEntry = {
sourceCode: sourceCode.text,
filename,
userOpts,
flowSuppressions: [],
events: [],
};
if (entry != null) {
Object.assign(entry, emptyResult);
} else {
cache.push(filename, emptyResult);
}
return {...emptyResult};
}
const runEntry = runReactCompilerImpl({
sourceCode,
filename,

View File

@@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => {
if (__DEV__) {
console.warn('Hello\n in div');
}
assertConsoleWarnDev(['Hello\n in div']);
assertConsoleWarnDev(['Hello']);
});
it('passes if all warnings contain a stack', () => {
@@ -888,11 +888,7 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Good day\n in div');
console.warn('Bye\n in div');
}
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
it('fails if act is called without assertConsoleWarnDev', async () => {
@@ -1079,11 +1075,7 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi \n in div');
console.warn('Wow \n in div');
assertConsoleWarnDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
@@ -1093,9 +1085,9 @@ describe('ReactInternalTestUtils console assertions', () => {
- Expected warnings
+ Received warnings
- Hi in div
- Wow in div
- Bye in div
- Hi
- Wow
- Bye
+ Hi in div (at **)
+ Wow in div (at **)"
`);
@@ -1196,26 +1188,16 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello');
console.warn('Good day\n in div');
console.warn('Bye\n in div');
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Missing component stack for:
"Hello"
- Expected warnings
+ Received warnings
- Hello in div
- Good day in div
- Bye in div
+ Hello
+ Good day in div (at **)
+ Bye in div (at **)"
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
`);
});
@@ -1225,26 +1207,16 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello\n in div');
console.warn('Good day');
console.warn('Bye\n in div');
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Missing component stack for:
"Good day"
- Expected warnings
+ Received warnings
- Hello in div
- Good day in div
- Bye in div
+ Hello in div (at **)
+ Good day
+ Bye in div (at **)"
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
`);
});
@@ -1254,26 +1226,41 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello\n in div');
console.warn('Good day\n in div');
console.warn('Bye');
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Missing component stack for:
"Bye"
- Expected warnings
+ Received warnings
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
`);
});
- Hello in div
- Good day in div
- Bye in div
+ Hello in div (at **)
+ Good day in div (at **)
+ Bye"
// @gate __DEV__
it('fails if all warnings do not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.warn('Hello');
console.warn('Good day');
console.warn('Bye');
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Missing component stack for:
"Hello"
Missing component stack for:
"Good day"
Missing component stack for:
"Bye"
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
`);
});
@@ -1352,13 +1339,12 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected warnings
+ Received warnings
- Hello
+ Hello in div (at **)"
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
`);
});
@@ -1375,16 +1361,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected warnings
+ Received warnings
Unexpected component stack for:
"Bye
in div (at **)"
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
`);
});
});
@@ -1396,9 +1382,9 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Bye\n in div');
}
assertConsoleWarnDev([
'Hello\n in div',
'Hello',
['Good day', {withoutStack: true}],
'Bye\n in div',
'Bye',
]);
});
@@ -1504,13 +1490,12 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected warnings
+ Received warnings
- Hello
+ Hello in div (at **)"
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
`);
});
@@ -1539,16 +1524,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected warnings
+ Received warnings
Unexpected component stack for:
"Bye
in div (at **)"
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
`);
});
});
@@ -1621,18 +1606,13 @@ describe('ReactInternalTestUtils console assertions', () => {
it('fails if component stack is passed twice', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi %s%s', '\n in div', '\n in div');
assertConsoleWarnDev(['Hi \n in div (at **)']);
assertConsoleWarnDev(['Hi']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
- Expected warnings
+ Received warnings
Hi in div (at **)
+ in div (at **)"
Received more than one component stack for a warning:
"Hi %s%s""
`);
});
@@ -1641,23 +1621,16 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi %s%s', '\n in div', '\n in div');
console.warn('Bye %s%s', '\n in div', '\n in div');
assertConsoleWarnDev([
'Hi \n in div (at **)',
'Bye \n in div (at **)',
]);
assertConsoleWarnDev(['Hi', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected warning(s) recorded.
Received more than one component stack for a warning:
"Hi %s%s"
- Expected warnings
+ Received warnings
Hi in div (at **)
+ in div (at **)
Bye in div (at **)
+ in div (at **)"
Received more than one component stack for a warning:
"Bye %s%s""
`);
});
@@ -1673,7 +1646,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
assertConsoleWarnDev(['Hi', 'Bye']);
});
// @gate __DEV__
@@ -1688,7 +1661,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
assertConsoleWarnDev(['Hi', 'Bye']);
});
// @gate __DEV__
@@ -1704,11 +1677,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
});
it('should fail if waitFor is called before asserting', async () => {
@@ -1915,7 +1884,7 @@ describe('ReactInternalTestUtils console assertions', () => {
if (__DEV__) {
console.error('Hello\n in div');
}
assertConsoleErrorDev(['Hello\n in div']);
assertConsoleErrorDev(['Hello']);
});
it('passes if all errors contain a stack', () => {
@@ -1924,11 +1893,7 @@ describe('ReactInternalTestUtils console assertions', () => {
console.error('Good day\n in div');
console.error('Bye\n in div');
}
assertConsoleErrorDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
it('fails if act is called without assertConsoleErrorDev', async () => {
@@ -2115,11 +2080,7 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi \n in div');
console.error('Wow \n in div');
assertConsoleErrorDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
@@ -2129,9 +2090,9 @@ describe('ReactInternalTestUtils console assertions', () => {
- Expected errors
+ Received errors
- Hi in div
- Wow in div
- Bye in div
- Hi
- Wow
- Bye
+ Hi in div (at **)
+ Wow in div (at **)"
`);
@@ -2231,6 +2192,101 @@ describe('ReactInternalTestUtils console assertions', () => {
+ TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
`);
});
// @gate __DEV__
it('fails if only error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
assertConsoleErrorDev(['Hello']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if first error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello\n in div');
console.error('Good day\n in div');
console.error('Bye');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Bye"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if last error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
console.error('Good day\n in div');
console.error('Bye\n in div');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if middle error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello\n in div');
console.error('Good day');
console.error('Bye\n in div');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Good day"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if all errors do not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
console.error('Good day');
console.error('Bye');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
Missing component stack for:
"Good day"
Missing component stack for:
"Bye"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('regression: checks entire string, not just the first letter', async () => {
@@ -2329,13 +2385,12 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected errors
+ Received errors
- Hello
+ Hello in div (at **)"
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
`);
});
@@ -2352,16 +2407,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected errors
+ Received errors
Unexpected component stack for:
"Bye
in div (at **)"
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
`);
});
});
@@ -2373,9 +2428,9 @@ describe('ReactInternalTestUtils console assertions', () => {
console.error('Bye\n in div');
}
assertConsoleErrorDev([
'Hello\n in div',
'Hello',
['Good day', {withoutStack: true}],
'Bye\n in div',
'Bye',
]);
});
@@ -2481,13 +2536,12 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected errors
+ Received errors
- Hello
+ Hello in div (at **)"
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
`);
});
@@ -2516,16 +2570,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
Unexpected component stack for:
"Hello
in div (at **)"
- Expected errors
+ Received errors
Unexpected component stack for:
"Bye
in div (at **)"
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
`);
});
@@ -2624,18 +2678,13 @@ describe('ReactInternalTestUtils console assertions', () => {
it('fails if component stack is passed twice', () => {
const message = expectToThrowFailure(() => {
console.error('Hi %s%s', '\n in div', '\n in div');
assertConsoleErrorDev(['Hi \n in div (at **)']);
assertConsoleErrorDev(['Hi']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
- Expected errors
+ Received errors
Hi in div (at **)
+ in div (at **)"
Received more than one component stack for a warning:
"Hi %s%s""
`);
});
@@ -2644,23 +2693,16 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi %s%s', '\n in div', '\n in div');
console.error('Bye %s%s', '\n in div', '\n in div');
assertConsoleErrorDev([
'Hi \n in div (at **)',
'Bye \n in div (at **)',
]);
assertConsoleErrorDev(['Hi', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected error(s) recorded.
Received more than one component stack for a warning:
"Hi %s%s"
- Expected errors
+ Received errors
Hi in div (at **)
+ in div (at **)
Bye in div (at **)
+ in div (at **)"
Received more than one component stack for a warning:
"Bye %s%s""
`);
});
@@ -2669,14 +2711,14 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi \n in div');
console.error('Bye \n in div');
assertConsoleErrorDev('Hi \n in div', 'Bye \n in div');
assertConsoleErrorDev('Hi', 'Bye');
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
assertConsoleErrorDev(['Hi', 'Bye']);
});
// @gate __DEV__
@@ -2691,7 +2733,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
assertConsoleErrorDev(['Hi', 'Bye']);
});
// @gate __DEV__
@@ -2707,133 +2749,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
});
describe('in <stack> placeholder', () => {
// @gate __DEV__
it('fails if `in <stack>` is used for a component stack instead of an error stack', () => {
const message = expectToThrowFailure(() => {
console.error('Warning message\n in div');
assertConsoleErrorDev(['Warning message\n in <stack>']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "Warning message
in <stack>"
Received: "Warning message
in div (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
`);
});
// @gate __DEV__
it('fails if `in <stack>` is used for multiple component stacks', () => {
const message = expectToThrowFailure(() => {
console.error('First warning\n in span');
console.error('Second warning\n in div');
assertConsoleErrorDev([
'First warning\n in <stack>',
'Second warning\n in <stack>',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "First warning
in <stack>"
Received: "First warning
in span (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)").
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "Second warning
in <stack>"
Received: "Second warning
in div (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
`);
});
it('allows `in <stack>` for actual error stack traces', () => {
// This should pass - \n in <stack> is correctly used for an error stack
console.error(new Error('Something went wrong'));
assertConsoleErrorDev(['Error: Something went wrong\n in <stack>']);
});
// @gate __DEV__
it('fails if error stack trace is present but \\n in <stack> is not expected', () => {
const message = expectToThrowFailure(() => {
console.error(new Error('Something went wrong'));
assertConsoleErrorDev(['Error: Something went wrong']);
});
expect(message).toMatch(`Unexpected error stack trace for:`);
expect(message).toMatch(`Error: Something went wrong`);
expect(message).toMatch(
'If this error should include an error stack trace, add \\n in <stack> to your expected message'
);
});
// @gate __DEV__
it('fails if `in <stack>` is expected but no stack is present', () => {
const message = expectToThrowFailure(() => {
console.error('Error: Something went wrong');
assertConsoleErrorDev([
'Error: Something went wrong\n in <stack>',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing error stack trace for:
"Error: Something went wrong"
The expected message uses \\n in <stack> but the actual error doesn't include an error stack trace.
If this error should not have an error stack trace, remove \\n in <stack> from your expected message."
`);
});
});
describe('[Environment] placeholder', () => {
// @gate __DEV__
it('expands [Server] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Server \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Server] Error: something went wrong', {withoutStack: true}],
]);
});
// @gate __DEV__
it('expands [Prerender] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Prerender] Error: something went wrong', {withoutStack: true}],
]);
});
// @gate __DEV__
it('expands [Cache] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Cache \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Cache] Error: something went wrong', {withoutStack: true}],
]);
});
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
});
it('should fail if waitFor is called before asserting', async () => {

View File

@@ -168,53 +168,6 @@ function normalizeCodeLocInfo(str) {
});
}
// Expands environment placeholders like [Server] into ANSI escape sequences.
// This allows test assertions to use a cleaner syntax like "[Server] Error:"
// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"
function expandEnvironmentPlaceholders(str) {
if (typeof str !== 'string') {
return str;
}
// [Environment] -> ANSI escape sequence for environment badge
// The format is: reset + inverse + " Environment " + reset
return str.replace(
/^\[(\w+)] /g,
(match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',
);
}
// The error stack placeholder that can be used in expected messages
const ERROR_STACK_PLACEHOLDER = '\n in <stack>';
// A marker used to protect the placeholder during normalization
const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';
// Normalizes expected messages, handling special placeholders
function normalizeExpectedMessage(str) {
if (typeof str !== 'string') {
return str;
}
// Protect the error stack placeholder from normalization
// (normalizeCodeLocInfo would add "(at **)" to it)
const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);
let result = str;
if (hasStackPlaceholder) {
result = result.replace(
ERROR_STACK_PLACEHOLDER,
ERROR_STACK_PLACEHOLDER_MARKER,
);
}
result = normalizeCodeLocInfo(result);
result = expandEnvironmentPlaceholders(result);
if (hasStackPlaceholder) {
// Restore the placeholder (remove the "(at **)" that was added)
result = result.replace(
ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',
ERROR_STACK_PLACEHOLDER,
);
}
return result;
}
function normalizeComponentStack(entry) {
if (
typeof entry[0] === 'string' &&
@@ -234,15 +187,6 @@ const isLikelyAComponentStack = message =>
message.includes('\n in ') ||
message.includes('\n at '));
// Error stack traces start with "*Error:" and contain "at" frames with file paths
// Component stacks contain "in ComponentName" patterns
// This helps validate that \n in <stack> is used correctly
const isLikelyAnErrorStackTrace = message =>
typeof message === 'string' &&
message.includes('Error:') &&
// Has "at" frames typical of error stacks (with file:line:col)
/\n\s+at .+\(.*:\d+:\d+\)/.test(message);
export function createLogAssertion(
consoleMethod,
matcherName,
@@ -292,11 +236,13 @@ export function createLogAssertion(
const withoutStack = options.withoutStack;
// Warn about invalid global withoutStack values.
if (consoleMethod === 'log' && withoutStack !== undefined) {
throwFormattedError(
`Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
);
} else if (withoutStack !== undefined && withoutStack !== true) {
// withoutStack can only have a value true.
throwFormattedError(
`The second argument must be {withoutStack: true}.` +
`\n\nInstead received ${JSON.stringify(options)}.`,
@@ -310,11 +256,8 @@ export function createLogAssertion(
const unexpectedLogs = [];
const unexpectedMissingComponentStack = [];
const unexpectedIncludingComponentStack = [];
const unexpectedMissingErrorStack = [];
const unexpectedIncludingErrorStack = [];
const logsMismatchingFormat = [];
const logsWithExtraComponentStack = [];
const stackTracePlaceholderMisuses = [];
// Loop over all the observed logs to determine:
// - Which expected logs are missing
@@ -376,11 +319,11 @@ export function createLogAssertion(
);
}
expectedMessage = normalizeExpectedMessage(currentExpectedMessage);
expectedMessage = normalizeCodeLocInfo(currentExpectedMessage);
expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
} else if (typeof expectedMessageOrArray === 'string') {
expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);
// withoutStack: inherit from global option - simplify when withoutStack is removed.
// Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray);
if (consoleMethod === 'log') {
expectedWithoutStack = true;
} else {
@@ -438,93 +381,19 @@ export function createLogAssertion(
}
// Main logic to check if log is expected, with the component stack.
// Check for exact match OR if the message matches with a component stack appended
let matchesExpectedMessage = false;
let expectsErrorStack = false;
const hasErrorStack = isLikelyAnErrorStackTrace(message);
if (typeof expectedMessage === 'string') {
if (normalizedMessage === expectedMessage) {
matchesExpectedMessage = true;
} else if (expectedMessage.includes('\n in <stack>')) {
expectsErrorStack = true;
// \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")
// NOT for React component stacks (e.g., "\n in ComponentName (at **)").
// Validate that the actual message looks like an error stack trace.
if (!hasErrorStack) {
// The actual message doesn't look like an error stack trace.
// This is likely a misuse - someone used \n in <stack> for a component stack.
stackTracePlaceholderMisuses.push({
expected: expectedMessage,
received: normalizedMessage,
});
}
const expectedMessageWithoutStack = expectedMessage.replace(
'\n in <stack>',
'',
);
if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {
// Remove the stack trace
const remainder = normalizedMessage.slice(
expectedMessageWithoutStack.length,
);
// After normalization, both error stacks and component stacks look like
// component stacks (at frames are converted to "in ... (at **)" format).
// So we check isLikelyAComponentStack for matching purposes.
if (isLikelyAComponentStack(remainder)) {
const messageWithoutStack = normalizedMessage.replace(
remainder,
'',
);
if (messageWithoutStack === expectedMessageWithoutStack) {
matchesExpectedMessage = true;
}
} else if (remainder === '') {
// \n in <stack> was expected but there's no stack at all
matchesExpectedMessage = true;
}
} else if (normalizedMessage === expectedMessageWithoutStack) {
// \n in <stack> was expected but actual has no stack at all (exact match without stack)
matchesExpectedMessage = true;
}
} else if (
hasErrorStack &&
!expectedMessage.includes('\n in <stack>') &&
normalizedMessage.startsWith(expectedMessage)
) {
matchesExpectedMessage = true;
}
}
if (matchesExpectedMessage) {
// withoutStack: Check for unexpected/missing component stacks.
// These checks can be simplified when withoutStack is removed.
if (
typeof expectedMessage === 'string' &&
(normalizedMessage === expectedMessage ||
normalizedMessage.includes(expectedMessage))
) {
if (isLikelyAComponentStack(normalizedMessage)) {
if (expectedWithoutStack === true && !hasErrorStack) {
// Only report unexpected component stack if it's not an error stack
// (error stacks look like component stacks after normalization)
if (expectedWithoutStack === true) {
unexpectedIncludingComponentStack.push(normalizedMessage);
}
} else if (expectedWithoutStack !== true && !expectsErrorStack) {
} else if (expectedWithoutStack !== true) {
unexpectedMissingComponentStack.push(normalizedMessage);
}
// Check for unexpected/missing error stacks
if (hasErrorStack && !expectsErrorStack) {
// Error stack is present but \n in <stack> was not in the expected message
unexpectedIncludingErrorStack.push(normalizedMessage);
} else if (
expectsErrorStack &&
!hasErrorStack &&
!isLikelyAComponentStack(normalizedMessage)
) {
// \n in <stack> was expected but the actual message doesn't have any stack at all
// (if it has a component stack, stackTracePlaceholderMisuses already handles it)
unexpectedMissingErrorStack.push(normalizedMessage);
}
// Found expected log, remove it from missing.
missingExpectedLogs.splice(0, 1);
} else {
@@ -553,21 +422,6 @@ export function createLogAssertion(
)}`;
}
// Wrong %s formatting is a failure.
// This is a common mistake when creating new warnings.
if (logsMismatchingFormat.length > 0) {
throwFormattedError(
logsMismatchingFormat
.map(
item =>
`Received ${item.args.length} arguments for a message with ${
item.expectedArgCount
} placeholders:\n ${printReceived(item.format)}`,
)
.join('\n\n'),
);
}
// Any unexpected warnings should be treated as a failure.
if (unexpectedLogs.length > 0) {
throwFormattedError(
@@ -612,33 +466,18 @@ export function createLogAssertion(
);
}
// Any logs that include an error stack trace but \n in <stack> wasn't expected.
if (unexpectedIncludingErrorStack.length > 0) {
// Wrong %s formatting is a failure.
// This is a common mistake when creating new warnings.
if (logsMismatchingFormat.length > 0) {
throwFormattedError(
`${unexpectedIncludingErrorStack
logsMismatchingFormat
.map(
stack =>
`Unexpected error stack trace for:\n ${printReceived(stack)}`,
item =>
`Received ${item.args.length} arguments for a message with ${
item.expectedArgCount
} placeholders:\n ${printReceived(item.format)}`,
)
.join(
'\n\n',
)}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +
`(e.g., "Error: message\\n in <stack>").`,
);
}
// Any logs that are missing an error stack trace when \n in <stack> was expected.
if (unexpectedMissingErrorStack.length > 0) {
throwFormattedError(
`${unexpectedMissingErrorStack
.map(
stack =>
`Missing error stack trace for:\n ${printReceived(stack)}`,
)
.join(
'\n\n',
)}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +
`\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,
.join('\n\n'),
);
}
@@ -657,25 +496,6 @@ export function createLogAssertion(
.join('\n\n'),
);
}
// Using \n in <stack> for component stacks is a misuse.
// \n in <stack> should only be used for JavaScript Error stack traces,
// not for React component stacks.
if (stackTracePlaceholderMisuses.length > 0) {
throwFormattedError(
`${stackTracePlaceholderMisuses
.map(
item =>
`Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +
`stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +
`Expected: ${printReceived(item.expected)}\n` +
`Received: ${printReceived(item.received)}\n\n` +
`If this ${logName()} has a component stack, include the full component stack in your expected message ` +
`(e.g., "Warning message\\n in ComponentName (at **)").`,
)
.join('\n\n')}`,
);
}
}
};
}

View File

@@ -79,18 +79,6 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
status: promise.status,
};
}
} else if ('value' in ioInfo) {
// If value exists in ioInfo but is undefined (e.g., WeakRef was GC'd),
// ensure we still include it in the normalized output for consistency
copy.value = {
value: undefined,
};
} else if (ioInfo.name && ioInfo.name !== 'rsc stream') {
// For non-rsc-stream IO that doesn't have a value field, add a default.
// This handles the case where the server doesn't send the field when WeakRef is GC'd.
copy.value = {
value: undefined,
};
}
return copy;
}

View File

@@ -549,13 +549,6 @@ export function startGestureTransition() {
export function stopViewTransition(transition: RunningViewTransition) {}
export function addViewTransitionFinishedListener(
transition: RunningViewTransition,
callback: () => void,
) {
callback();
}
export type ViewTransitionInstance = null | {name: string, ...};
export function createViewTransitionInstance(

View File

@@ -1085,6 +1085,7 @@ describe('ReactFlight', () => {
});
});
// @gate renameElementSymbol
it('should emit descriptions of errors in dev', async () => {
const ClientErrorBoundary = clientReference(ErrorBoundary);
@@ -1728,8 +1729,7 @@ describe('ReactFlight', () => {
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
' <... value={{}}>\n' +
' ^^^^\n' +
' in (at **)',
' ^^^^\n',
]);
});
@@ -3258,7 +3258,7 @@ describe('ReactFlight', () => {
const transport = ReactNoopFlightServer.render({
root: ReactServer.createElement(App),
});
assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
assertConsoleErrorDev(['Error: err']);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');

View File

@@ -467,11 +467,9 @@ function useSyncExternalStore<T>(
// useSyncExternalStore() composes multiple hooks internally.
// Advance the current hook index the same number of times
// so that subsequent hooks have the right memoized state.
const hook = nextHook(); // SyncExternalStore
nextHook(); // SyncExternalStore
nextHook(); // Effect
// Read from hook.memoizedState to get the value that was used during render,
// not the current value from getSnapshot() which may have changed.
const value = hook !== null ? hook.memoizedState : getSnapshot();
const value = getSnapshot();
hookLog.push({
displayName: null,
primitive: 'SyncExternalStore',

View File

@@ -734,11 +734,7 @@ describe('ReactHooksInspection', () => {
});
const results = normalizeSourceLoc(tree);
expect(results).toHaveLength(1);
expect(results[0]).toMatchInlineSnapshot(
{
subHooks: [{value: expect.any(Promise)}],
},
`
expect(results[0]).toMatchInlineSnapshot(`
{
"debugInfo": null,
"hookSource": {
@@ -763,13 +759,12 @@ describe('ReactHooksInspection', () => {
"isStateEditable": false,
"name": "Use",
"subHooks": [],
"value": Any<Promise>,
"value": Promise {},
},
],
"value": undefined,
}
`,
);
`);
});
describe('useDebugValue', () => {

View File

@@ -293,7 +293,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
scheduleRetry();
}
function handleMessage(event: MessageEvent<>) {
function handleMessage(event: MessageEvent) {
let data;
try {
if (typeof event.data === 'string') {

View File

@@ -17,14 +17,6 @@ const contentScriptsToInject = [
runAt: 'document_end',
world: chrome.scripting.ExecutionWorld.ISOLATED,
},
{
id: '@react-devtools/fallback-eval-context',
js: ['build/fallbackEvalContext.js'],
matches: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
{
id: '@react-devtools/hook',
js: ['build/installHook.js'],

View File

@@ -97,58 +97,6 @@ export function handleDevToolsPageMessage(message) {
break;
}
case 'eval-in-inspected-window': {
const {
payload: {tabId, requestId, scriptId, args},
} = message;
chrome.tabs
.sendMessage(tabId, {
source: 'devtools-page-eval',
payload: {
scriptId,
args,
},
})
.then(response => {
if (!response) {
chrome.runtime.sendMessage({
source: 'react-devtools-background',
payload: {
type: 'eval-in-inspected-window-response',
requestId,
result: null,
error: 'No response from content script',
},
});
return;
}
const {result, error} = response;
chrome.runtime.sendMessage({
source: 'react-devtools-background',
payload: {
type: 'eval-in-inspected-window-response',
requestId,
result,
error,
},
});
})
.catch(error => {
chrome.runtime.sendMessage({
source: 'react-devtools-background',
payload: {
type: 'eval-in-inspected-window-response',
requestId,
result: null,
error: error?.message || String(error),
},
});
});
break;
}
}
}

View File

@@ -1,35 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {evalScripts} from '../evalScripts';
window.addEventListener('message', event => {
if (event.data?.source === 'react-devtools-content-script-eval') {
const {scriptId, args, requestId} = event.data.payload;
const response = {result: null, error: null};
try {
if (!evalScripts[scriptId]) {
throw new Error(`No eval script with id "${scriptId}" exists.`);
}
response.result = evalScripts[scriptId].fn.apply(null, args);
} catch (err) {
response.error = err.message;
}
window.postMessage(
{
source: 'react-devtools-content-script-eval-response',
payload: {
requestId,
response,
},
},
'*',
);
}
});

View File

@@ -1,50 +1,38 @@
/* global chrome */
/** @flow */
// We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN
// This is the only purpose of this script - to send persisted settings to installHook.js content script
import type {UnknownMessageEvent} from './messages';
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
import {postMessage} from './messages';
async function messageListener(event: UnknownMessageEvent) {
async function messageListener(event: MessageEvent) {
if (event.source !== window) {
return;
}
if (event.data.source === 'react-devtools-hook-installer') {
if (event.data.payload.handshake) {
const settings: Partial<DevToolsHookSettings> =
await chrome.storage.local.get();
const settings = await chrome.storage.local.get();
// If storage was empty (first installation), define default settings
const hookSettings: DevToolsHookSettings = {
appendComponentStack:
typeof settings.appendComponentStack === 'boolean'
? settings.appendComponentStack
: true,
breakOnConsoleErrors:
typeof settings.breakOnConsoleErrors === 'boolean'
? settings.breakOnConsoleErrors
: false,
showInlineWarningsAndErrors:
typeof settings.showInlineWarningsAndErrors === 'boolean'
? settings.showInlineWarningsAndErrors
: true,
hideConsoleLogsInStrictMode:
typeof settings.hideConsoleLogsInStrictMode === 'boolean'
? settings.hideConsoleLogsInStrictMode
: false,
disableSecondConsoleLogDimmingInStrictMode:
typeof settings.disableSecondConsoleLogDimmingInStrictMode ===
'boolean'
? settings.disableSecondConsoleLogDimmingInStrictMode
: false,
};
if (typeof settings.appendComponentStack !== 'boolean') {
settings.appendComponentStack = true;
}
if (typeof settings.breakOnConsoleErrors !== 'boolean') {
settings.breakOnConsoleErrors = false;
}
if (typeof settings.showInlineWarningsAndErrors !== 'boolean') {
settings.showInlineWarningsAndErrors = true;
}
if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') {
settings.hideConsoleLogsInStrictMode = false;
}
if (
typeof settings.disableSecondConsoleLogDimmingInStrictMode !== 'boolean'
) {
settings.disableSecondConsoleLogDimmingInStrictMode = false;
}
postMessage({
window.postMessage({
source: 'react-devtools-hook-settings-injector',
payload: {settings: hookSettings},
payload: {settings},
});
window.removeEventListener('message', messageListener);
@@ -53,7 +41,7 @@ async function messageListener(event: UnknownMessageEvent) {
}
window.addEventListener('message', messageListener);
postMessage({
window.postMessage({
source: 'react-devtools-hook-settings-injector',
payload: {handshake: true},
});

View File

@@ -1,46 +1,39 @@
/** @flow */
import type {UnknownMessageEvent} from './messages';
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
import {installHook} from 'react-devtools-shared/src/hook';
import {
getIfReloadedAndProfiling,
getProfilingSettings,
} from 'react-devtools-shared/src/utils';
import {postMessage} from './messages';
let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void;
let resolveHookSettingsInjection;
function messageListener(event: UnknownMessageEvent) {
function messageListener(event: MessageEvent) {
if (event.source !== window) {
return;
}
if (event.data.source === 'react-devtools-hook-settings-injector') {
const payload = event.data.payload;
// In case handshake message was sent prior to hookSettingsInjector execution
// We can't guarantee order
if (payload.handshake) {
postMessage({
if (event.data.payload.handshake) {
window.postMessage({
source: 'react-devtools-hook-installer',
payload: {handshake: true},
});
} else if (payload.settings) {
} else if (event.data.payload.settings) {
window.removeEventListener('message', messageListener);
resolveHookSettingsInjection(payload.settings);
resolveHookSettingsInjection(event.data.payload.settings);
}
}
}
// Avoid double execution
if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
const hookSettingsPromise = new Promise<DevToolsHookSettings>(resolve => {
const hookSettingsPromise = new Promise(resolve => {
resolveHookSettingsInjection = resolve;
});
window.addEventListener('message', messageListener);
postMessage({
window.postMessage({
source: 'react-devtools-hook-installer',
payload: {handshake: true},
});

View File

@@ -1,42 +0,0 @@
/** @flow */
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
export function postMessage(event: UnknownMessageEventData): void {
window.postMessage(event);
}
export interface UnknownMessageEvent
extends MessageEvent<UnknownMessageEventData> {}
export type UnknownMessageEventData =
| HookSettingsInjectorEventData
| HookInstallerEventData;
export type HookInstallerEventData = {
source: 'react-devtools-hook-installer',
payload: HookInstallerEventPayload,
};
export type HookInstallerEventPayload = HookInstallerEventPayloadHandshake;
export type HookInstallerEventPayloadHandshake = {
handshake: true,
};
export type HookSettingsInjectorEventData = {
source: 'react-devtools-hook-settings-injector',
payload: HookSettingsInjectorEventPayload,
};
export type HookSettingsInjectorEventPayload =
| HookSettingsInjectorEventPayloadHandshake
| HookSettingsInjectorEventPayloadSettings;
export type HookSettingsInjectorEventPayloadHandshake = {
handshake: true,
};
export type HookSettingsInjectorEventPayloadSettings = {
settings: DevToolsHookSettings,
};

View File

@@ -117,49 +117,3 @@ function connectPort() {
// $FlowFixMe[incompatible-use]
port.onDisconnect.addListener(handleDisconnect);
}
let evalRequestId = 0;
const evalRequestCallbacks = new Map<number, Function>();
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg?.source) {
case 'devtools-page-eval': {
const {scriptId, args} = msg.payload;
const requestId = evalRequestId++;
window.postMessage(
{
source: 'react-devtools-content-script-eval',
payload: {
requestId,
scriptId,
args,
},
},
'*',
);
evalRequestCallbacks.set(requestId, sendResponse);
return true; // Indicate we will respond asynchronously
}
}
});
window.addEventListener('message', event => {
if (event.data?.source === 'react-devtools-content-script-eval-response') {
const {requestId, response} = event.data.payload;
const callback = evalRequestCallbacks.get(requestId);
try {
if (!callback)
throw new Error(
`No eval request callback for id "${requestId}" exists.`,
);
callback(response);
} catch (e) {
console.warn(
'React DevTools Content Script eval response error occurred:',
e,
);
} finally {
evalRequestCallbacks.delete(requestId);
}
}
});

View File

@@ -1,112 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type EvalScriptIds =
| 'checkIfReactPresentInInspectedWindow'
| 'reload'
| 'setBrowserSelectionFromReact'
| 'setReactSelectionFromBrowser'
| 'viewAttributeSource'
| 'viewElementSource';
/*
.fn for fallback in Content Script context
.code for chrome.devtools.inspectedWindow.eval()
*/
type EvalScriptEntry = {
fn: (...args: any[]) => any,
code: (...args: any[]) => string,
};
/*
Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
So some fallback functions are no-op or throw error.
*/
export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
checkIfReactPresentInInspectedWindow: {
fn: () =>
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
code: () =>
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
},
reload: {
fn: () => window.location.reload(),
code: () => 'window.location.reload();',
},
setBrowserSelectionFromReact: {
fn: () => {
throw new Error('Not supported in fallback eval context');
},
code: () =>
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
'false',
},
setReactSelectionFromBrowser: {
fn: () => {
throw new Error('Not supported in fallback eval context');
},
code: () =>
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
'false',
},
viewAttributeSource: {
fn: ({rendererID, elementID, path}) => {
return false; // Not supported in fallback eval context
},
code: ({rendererID, elementID, path}) =>
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementAttributeByPath(' +
JSON.stringify(elementID) +
',' +
JSON.stringify(path) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
},
viewElementSource: {
fn: ({rendererID, elementID}) => {
return false; // Not supported in fallback eval context
},
code: ({rendererID, elementID}) =>
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementSourceFunctionById(' +
JSON.stringify(elementID) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
},
};

View File

@@ -1,12 +1,13 @@
import {evalInInspectedWindow} from './evalInInspectedWindow';
/* global chrome */
export function setBrowserSelectionFromReact() {
// This is currently only called on demand when you press "view DOM".
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
// we could make this happen automatically when you select another component.
evalInInspectedWindow(
'setBrowserSelectionFromReact',
[],
chrome.devtools.inspectedWindow.eval(
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
'false',
(didSelectionChange, evalError) => {
if (evalError) {
console.error(evalError);
@@ -18,9 +19,10 @@ export function setBrowserSelectionFromReact() {
export function setReactSelectionFromBrowser(bridge) {
// When the user chooses a different node in the browser Elements tab,
// copy it over to the hook object so that we can sync the selection.
evalInInspectedWindow(
'setReactSelectionFromBrowser',
[],
chrome.devtools.inspectedWindow.eval(
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
'false',
(didSelectionChange, evalError) => {
if (evalError) {
console.error(evalError);
@@ -32,7 +34,7 @@ export function setReactSelectionFromBrowser(bridge) {
return;
}
// Remember to sync the selection next time we show inspected element
// Remember to sync the selection next time we show Components tab.
bridge.send('syncSelectionFromBuiltinElementsPanel');
}
},

View File

@@ -1,116 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {EvalScriptIds} from '../evalScripts';
import {evalScripts} from '../evalScripts';
type ExceptionInfo = {
code: ?string,
description: ?string,
isError: boolean,
isException: boolean,
value: any,
};
const EVAL_TIMEOUT = 1000 * 10;
let evalRequestId = 0;
const evalRequestCallbacks = new Map<
number,
(value: {result: any, error: any}) => void,
>();
function fallbackEvalInInspectedWindow(
scriptId: EvalScriptIds,
args: any[],
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
) {
if (!evalScripts[scriptId]) {
throw new Error(`No eval script with id "${scriptId}" exists.`);
}
const code = evalScripts[scriptId].code.apply(null, args);
const tabId = chrome.devtools.inspectedWindow.tabId;
const requestId = evalRequestId++;
chrome.runtime.sendMessage({
source: 'devtools-page',
payload: {
type: 'eval-in-inspected-window',
tabId,
requestId,
scriptId,
args,
},
});
const timeout = setTimeout(() => {
evalRequestCallbacks.delete(requestId);
if (callback) {
callback(null, {
code,
description:
'Timed out while waiting for eval response from the inspected window.',
isError: true,
isException: false,
value: undefined,
});
}
}, EVAL_TIMEOUT);
evalRequestCallbacks.set(requestId, ({result, error}) => {
clearTimeout(timeout);
evalRequestCallbacks.delete(requestId);
if (callback) {
if (error) {
callback(null, {
code,
description: undefined,
isError: false,
isException: true,
value: error,
});
return;
}
callback(result, null);
}
});
}
export function evalInInspectedWindow(
scriptId: EvalScriptIds,
args: any[],
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
) {
if (!evalScripts[scriptId]) {
throw new Error(`No eval script with id "${scriptId}" exists.`);
}
const code = evalScripts[scriptId].code.apply(null, args);
chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
if (!exceptionInfo) {
callback(result, exceptionInfo);
return;
}
// If an exception (e.g. CSP Blocked) occurred,
// fallback to the content script eval context
fallbackEvalInInspectedWindow(scriptId, args, callback);
});
}
chrome.runtime.onMessage.addListener(({payload, source}) => {
if (source === 'react-devtools-background') {
switch (payload?.type) {
case 'eval-in-inspected-window-response': {
const {requestId, result, error} = payload;
const callback = evalRequestCallbacks.get(requestId);
if (callback) {
callback({result, error});
}
break;
}
}
}
});

View File

@@ -1,14 +1,6 @@
/* global chrome */
/** @flow */
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
import type {
TabID,
ViewElementSource,
} from 'react-devtools-shared/src/devtools/views/DevTools';
import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
import type {Element} from 'react-devtools-shared/src/frontend/types';
import {createElement} from 'react';
import {flushSync} from 'react-dom';
@@ -40,7 +32,6 @@ import {
} from './elementSelection';
import {viewAttributeSource} from './sourceSelection';
import {evalInInspectedWindow} from './evalInInspectedWindow';
import {startReactPolling} from './reactPolling';
import {cloneStyleTags} from './cloneStyleTags';
import fetchFileWithCaching from './fetchFileWithCaching';
@@ -59,9 +50,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
function createBridge() {
bridge = new Bridge({
listen(fn) {
const bridgeListener = (message: Message) => fn(message);
const bridgeListener = message => fn(message);
// Store the reference so that we unsubscribe from the same object.
const portOnMessage = ((port: any): ExtensionPort).onMessage;
const portOnMessage = port.onMessage;
portOnMessage.addListener(bridgeListener);
lastSubscribedBridgeListener = bridgeListener;
@@ -79,7 +70,7 @@ function createBridge() {
bridge.addListener('reloadAppForProfiling', () => {
localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
evalInInspectedWindow('reload', [], () => {});
chrome.devtools.inspectedWindow.eval('window.location.reload();');
});
bridge.addListener(
@@ -184,20 +175,14 @@ function createBridgeAndStore() {
// Otherwise, the Store may miss important initial tree op codes.
injectBackendManager(chrome.devtools.inspectedWindow.tabId);
const viewAttributeSourceFunction = (
id: Element['id'],
path: Array<string | number>,
) => {
const viewAttributeSourceFunction = (id, path) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
viewAttributeSource(rendererID, id, path);
}
};
const viewElementSourceFunction: ViewElementSource = (
source,
symbolicatedSource,
) => {
const viewElementSourceFunction = (source, symbolicatedSource) => {
const [, sourceURL, line, column] = symbolicatedSource
? symbolicatedSource
: source;
@@ -212,7 +197,7 @@ function createBridgeAndStore() {
root = createRoot(document.createElement('div'));
render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
render = (overrideTab = mostRecentOverrideTab) => {
mostRecentOverrideTab = overrideTab;
root.render(
@@ -220,7 +205,6 @@ function createBridgeAndStore() {
bridge,
browserTheme: getBrowserTheme(),
componentsPortalContainer,
inspectedElementPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
@@ -241,9 +225,7 @@ function createBridgeAndStore() {
};
}
function ensureInitialHTMLIsCleared(
container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
) {
function ensureInitialHTMLIsCleared(container) {
if (container._hasInitialHTMLBeenCleared) {
return;
}
@@ -295,52 +277,6 @@ function createComponentsPanel() {
);
}
function createElementsInspectPanel() {
if (inspectedElementPortalContainer) {
// Panel is created and user opened it at least once
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
render();
return;
}
if (inspectedElementPane) {
// Panel is created, but wasn't opened yet, so no document is present for it
return;
}
const elementsPanel = chrome.devtools.panels.elements;
if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
// Firefox will not pass the window to the onShown listener despite setPage
// being called.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=2010549
// May not be supported in some browsers.
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels/ElementsPanel/createSidebarPane#browser_compatibility
return;
}
elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
inspectedElementPane = createdPane;
createdPane.setPage('panel.html');
createdPane.setHeight('75px');
createdPane.onShown.addListener(portal => {
inspectedElementPortalContainer = portal.container;
if (inspectedElementPortalContainer != null && render) {
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
bridge.send('syncSelectionFromBuiltinElementsPanel');
render();
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-inspected-element-pane'});
}
});
});
}
function createProfilerPanel() {
if (profilerPortalContainer) {
// Panel is created and user opened it at least once
@@ -414,6 +350,13 @@ function createSourcesEditorPanel() {
logEvent({event_name: 'selected-editor-pane'});
}
});
createdPane.onShown.addListener(() => {
bridge.emit('extensionEditorPaneShown');
});
createdPane.onHidden.addListener(() => {
bridge.emit('extensionEditorPaneHidden');
});
});
}
@@ -489,10 +432,10 @@ function performInTabNavigationCleanup() {
// Do not clean mostRecentOverrideTab on purpose, so we remember last opened
// React DevTools tab, when user does in-tab navigation
store = (null: $FlowFixMe);
bridge = (null: $FlowFixMe);
render = (null: $FlowFixMe);
root = (null: $FlowFixMe);
store = null;
bridge = null;
render = null;
root = null;
}
function performFullCleanup() {
@@ -514,18 +457,18 @@ function performFullCleanup() {
componentsPortalContainer = null;
profilerPortalContainer = null;
suspensePortalContainer = null;
root = (null: $FlowFixMe);
root = null;
mostRecentOverrideTab = null;
store = (null: $FlowFixMe);
bridge = (null: $FlowFixMe);
render = (null: $FlowFixMe);
store = null;
bridge = null;
render = null;
port?.disconnect();
port = (null: $FlowFixMe);
port = null;
}
function connectExtensionPort(): void {
function connectExtensionPort() {
if (port) {
throw new Error('DevTools port was already connected');
}
@@ -549,7 +492,7 @@ function connectExtensionPort(): void {
// so, when we call `port.disconnect()` from this script,
// this should not trigger this callback and port reconnection
port.onDisconnect.addListener(() => {
port = (null: $FlowFixMe);
port = null;
connectExtensionPort();
});
}
@@ -564,7 +507,6 @@ function mountReactDevTools() {
createComponentsPanel();
createProfilerPanel();
createSourcesEditorPanel();
createElementsInspectPanel();
// Suspense Tab is created via the hook
// TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable
}
@@ -603,9 +545,9 @@ function mountReactDevToolsWhenReactHasLoaded() {
);
}
let bridge: FrontendBridge = (null: $FlowFixMe);
let bridge = null;
let lastSubscribedBridgeListener = null;
let store: Store = (null: $FlowFixMe);
let store = null;
let profilingData = null;
@@ -613,35 +555,18 @@ let componentsPanel = null;
let profilerPanel = null;
let suspensePanel = null;
let editorPane = null;
let inspectedElementPane = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let suspensePortalContainer = null;
let editorPortalContainer = null;
let inspectedElementPortalContainer = null;
let mostRecentOverrideTab: null | TabID = null;
let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
let root: RootType = (null: $FlowFixMe);
let mostRecentOverrideTab = null;
let render = null;
let root = null;
let currentSelectedSource: null | SourceSelection = null;
type ExtensionEvent = {
addListener(callback: (message: Message, port: ExtensionPort) => void): void,
removeListener(
callback: (message: Message, port: ExtensionPort) => void,
): void,
};
/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
type ExtensionPort = {
onDisconnect: ExtensionEvent,
onMessage: ExtensionEvent,
postMessage(message: mixed, transferable?: Array<mixed>): void,
disconnect(): void,
};
let port: ExtensionPort = (null: $FlowFixMe);
let port = null;
// In case when multiple navigation events emitted in a short period of time
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
@@ -674,7 +599,7 @@ connectExtensionPort();
mountReactDevToolsWhenReactHasLoaded();
function onThemeChanged() {
function onThemeChanged(themeName) {
// Rerender with the new theme
render();
}
@@ -711,12 +636,6 @@ if (chrome.devtools.panels.setOpenResourceHandler) {
resource.url,
lineNumber - 1,
columnNumber - 1,
maybeError => {
if (maybeError && maybeError.isError) {
// Not a resource Chrome can open. Fallback to browser default behavior.
window.open(resource.url);
}
},
);
},
);

View File

@@ -1,4 +1,4 @@
import {evalInInspectedWindow} from './evalInInspectedWindow';
/* global chrome */
class CouldNotFindReactOnThePageError extends Error {
constructor() {
@@ -26,9 +26,8 @@ export function startReactPolling(
// This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
evalInInspectedWindow(
'checkIfReactPresentInInspectedWindow',
[],
chrome.devtools.inspectedWindow.eval(
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
(pageHasReact, exceptionInfo) => {
if (status === 'aborted') {
onError(

View File

@@ -1,9 +1,27 @@
import {evalInInspectedWindow} from './evalInInspectedWindow';
/* global chrome */
export function viewAttributeSource(rendererID, elementID, path) {
evalInInspectedWindow(
'viewAttributeSource',
[{rendererID, elementID, path}],
chrome.devtools.inspectedWindow.eval(
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementAttributeByPath(' +
JSON.stringify(elementID) +
',' +
JSON.stringify(path) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
(didInspect, evalError) => {
if (evalError) {
console.error(evalError);
@@ -13,9 +31,25 @@ export function viewAttributeSource(rendererID, elementID, path) {
}
export function viewElementSource(rendererID, elementID) {
evalInInspectedWindow(
'viewElementSource',
[{rendererID, elementID}],
chrome.devtools.inspectedWindow.eval(
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementSourceFunctionById(' +
JSON.stringify(elementID) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
(didInspect, evalError) => {
if (evalError) {
console.error(evalError);

View File

@@ -69,7 +69,6 @@ module.exports = {
backend: './src/backend.js',
background: './src/background/index.js',
backendManager: './src/contentScripts/backendManager.js',
fallbackEvalContext: './src/contentScripts/fallbackEvalContext.js',
fileFetcher: './src/contentScripts/fileFetcher.js',
main: './src/main/index.js',
panel: './src/panel.js',

View File

@@ -6,10 +6,7 @@ import {initBackend} from 'react-devtools-shared/src/backend';
import {installHook} from 'react-devtools-shared/src/hook';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import type {
BackendBridge,
SavedPreferencesParams,
} from 'react-devtools-shared/src/bridge';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {Wall} from 'react-devtools-shared/src/frontend/types';
import {
getIfReloadedAndProfiling,
@@ -19,14 +16,31 @@ import {
} from 'react-devtools-shared/src/utils';
function startActivation(contentWindow: any, bridge: BackendBridge) {
const onSavedPreferences = (data: SavedPreferencesParams) => {
const onSavedPreferences = (data: $FlowFixMe) => {
// This is the only message we're listening for,
// so it's safe to cleanup after we've received it.
bridge.removeListener('savedPreferences', onSavedPreferences);
const {componentFilters} = data;
const {
appendComponentStack,
breakOnConsoleErrors,
componentFilters,
showInlineWarningsAndErrors,
hideConsoleLogsInStrictMode,
disableSecondConsoleLogDimmingInStrictMode,
} = data;
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ =
appendComponentStack;
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ =
breakOnConsoleErrors;
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
showInlineWarningsAndErrors;
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
hideConsoleLogsInStrictMode;
contentWindow.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ =
disableSecondConsoleLogDimmingInStrictMode;
// TRICKY
// The backend entry point may be required in the context of an iframe or the parent window.
@@ -35,7 +49,15 @@ function startActivation(contentWindow: any, bridge: BackendBridge) {
// Technically we don't need to store them on the contentWindow in this case,
// but it doesn't really hurt anything to store them there too.
if (contentWindow !== window) {
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
showInlineWarningsAndErrors;
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
hideConsoleLogsInStrictMode;
window.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ =
disableSecondConsoleLogDimmingInStrictMode;
}
finishActivation(contentWindow, bridge);

Some files were not shown because too many files have changed in this diff Show More