Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Abramov f51ad74b45 Fix CommonJS imports after Rollup upgrade 2023-03-24 23:59:59 +00:00
544 changed files with 16553 additions and 25095 deletions

View File

@ -426,6 +426,7 @@ jobs:
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
workflows:
version: 2
build_and_test:
unless: << pipeline.parameters.prerelease_commit_sha >>
@ -604,20 +605,10 @@ workflows:
when: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- publish_prerelease:
name: Publish to Canary channel
name: Publish to Next channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: stable
# The tags to use when publishing canaries. The main one we should
# always include is "canary" but we can use multiple (e.g. alpha,
# beta, rc). To declare multiple, use a comma-separated string, like
# this:
# dist_tag: "canary,alpha,beta,rc"
#
# TODO: We currently tag canaries with "next" in addition to "canary"
# because this used to be called the "next" channel and some
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: "canary,next"
dist_tag: "next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -625,7 +616,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Canary channel
- Publish to Next channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: experimental
dist_tag: experimental
@ -643,10 +634,10 @@ workflows:
- main
jobs:
- publish_prerelease:
name: Publish to Canary channel
name: Publish to Next channel
commit_sha: << pipeline.git.revision >>
release_channel: stable
dist_tag: "canary,next"
dist_tag: "next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -654,7 +645,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Canary channel
- Publish to Next channel
commit_sha: << pipeline.git.revision >>
release_channel: experimental
dist_tag: experimental

View File

@ -236,14 +236,7 @@ module.exports = {
'no-inner-declarations': [ERROR, 'functions'],
'no-multi-spaces': ERROR,
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
'no-restricted-syntax': [
ERROR,
'WithStatement',
{
selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]',
message: 'Prefer string.slice() over .substring() and .substr().',
},
],
'no-restricted-syntax': [ERROR, 'WithStatement'],
'no-shadow': ERROR,
'no-unused-vars': [ERROR, {args: 'none'}],
'no-use-before-define': OFF,
@ -416,6 +409,7 @@ module.exports = {
{
files: [
'packages/react-native-renderer/**/*.js',
'packages/react-server-native-relay/**/*.js',
],
globals: {
nativeFabricUIManager: 'readonly',
@ -455,9 +449,6 @@ module.exports = {
$ReadOnlyArray: 'readonly',
$Shape: 'readonly',
AnimationFrameID: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',

View File

@ -1,22 +1,13 @@
name: Commit Artifacts for Meta WWW and fbsource
name: Commit Artifacts for Facebook WWW and fbsource
on:
push:
branches: [main, meta-www, meta-fbsource]
branches: [main]
jobs:
download_artifacts:
runs-on: ubuntu-latest
outputs:
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
steps:
- uses: actions/checkout@v3
- name: "Check branches"
id: check_branches
run: |
echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
- name: Download and unzip artifacts
uses: actions/github-script@v6
env:
@ -177,7 +168,6 @@ jobs:
commit_www_artifacts:
needs: download_artifacts
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -214,7 +204,6 @@ jobs:
commit_fbsource_artifacts:
needs: download_artifacts
runs-on: ubuntu-latest
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
steps:
- uses: actions/checkout@v3
with:
@ -228,16 +217,7 @@ jobs:
name: compiled-rn
path: compiled-rn/
- run: git status -u
- name: Check if only the REVISION file has changed
id: check_should_commit
run: |
if git status --porcelain | grep -qv '/REVISION$'; then
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Commit changes to branch
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |

View File

@ -1,4 +1,4 @@
# [React](https://reactjs.org/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
# [React](https://reactjs.org/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
React is a JavaScript library for building user interfaces.

View File

@ -7,12 +7,12 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 18.3.0
// 18.0.0
//
// The @canary channel appends additional information, with the scheme
// The @next channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 18.3.0-canary-a1c2d3e4
// 18.0.0-alpha-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
@ -20,13 +20,9 @@
const ReactVersion = '18.3.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
// choose to change it to "alpha", "beta", "rc", etc.
//
// It only affects the label used in the version string. To customize the
// npm dist tags used during publish, refer to .circleci/config.yml.
const canaryChannelLabel = 'canary';
// The label used by the @next channel. Represents the upcoming release's
// stability. Could be "alpha", "beta", "rc", etc.
const nextChannelLabel = 'next';
const stablePackages = {
'eslint-plugin-react-hooks': '5.0.0',
@ -44,14 +40,14 @@ const stablePackages = {
scheduler: '0.24.0',
};
// These packages do not exist in the @canary or @latest channel, only
// These packages do not exist in the @next or @latest channel, only
// @experimental. We don't use semver, just the commit sha, so this is just a
// list of package names instead of a map.
const experimentalPackages = [];
module.exports = {
ReactVersion,
canaryChannelLabel,
nextChannelLabel,
stablePackages,
experimentalPackages,
};

View File

@ -22,7 +22,7 @@ class App extends PureComponent {
}
const multiplier = input.length !== 0 ? input.length : 1;
const complexity =
(parseInt(window.location.search.slice(1), 10) / 100) * 25 || 25;
(parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
const data = _.range(5).map(t =>
_.range(complexity * multiplier).map((j, i) => {
return {

View File

@ -15,8 +15,8 @@
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -11,12 +11,12 @@
__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
</script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@next/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -1,4 +1,4 @@
/* eslint-disable react/react-in-jsx-scope, react/jsx-no-undef */
/* eslint-disable no-fallthrough, react/react-in-jsx-scope, react/jsx-no-undef */
/* global React ReactCache ReactDOM SchedulerTracing ScheduleTracing */
const apps = [];

View File

@ -334,11 +334,6 @@
},
});
class Foo {
flag = false;
object = {a: {b: {c: {d: 1}}}}
}
function UnserializableProps() {
return (
<ChildComponent
@ -348,7 +343,6 @@
setOfSets={setOfSets}
typedArray={typedArray}
immutable={immutable}
classInstance={new Foo()}
/>
);
}

View File

@ -11,7 +11,7 @@ import semver from 'semver';
function parseQuery(qstr) {
var query = {};
var a = qstr.slice(1).split('&');
var a = qstr.substr(1).split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');

View File

@ -95,8 +95,6 @@ app.all('/', async function (req, res, next) {
if (req.get('rsc-action')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
proxiedHeaders['rsc-action'] = req.get('rsc-action');
} else if (req.get('Content-type')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
}
const promiseForData = request(

View File

@ -36,7 +36,6 @@ const bodyParser = require('body-parser');
const busboy = require('busboy');
const app = express();
const compress = require('compression');
const {Readable} = require('node:stream');
app.use(compress());
@ -46,7 +45,7 @@ const {readFile} = require('fs').promises;
const React = require('react');
async function renderApp(res, returnValue) {
app.get('/', async function (req, res) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@ -92,74 +91,37 @@ async function renderApp(res, returnValue) {
),
React.createElement(App),
];
// For client-invoked server actions we refresh the tree and return a return value.
const payload = returnValue ? {returnValue, root} : root;
const {pipe} = renderToPipeableStream(payload, moduleMap);
const {pipe} = renderToPipeableStream(root, moduleMap);
pipe(res);
}
app.get('/', async function (req, res) {
await renderApp(res, null);
});
app.post('/', bodyParser.text(), async function (req, res) {
const {
renderToPipeableStream,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
} = await import('react-server-dom-webpack/server');
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
await import('react-server-dom-webpack/server');
const serverReference = req.get('rsc-action');
if (serverReference) {
// This is the client-side case
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
} else {
args = await decodeReply(req.body);
}
const result = action.apply(null, args);
try {
// Wait for any mutations
await result;
} catch (x) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result);
} else {
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
const fakeRequest = new UndiciRequest('http://localhost', {
method: 'POST',
headers: {'Content-Type': req.headers['content-type']},
body: Readable.toWeb(req),
duplex: 'half',
});
const formData = await fakeRequest.formData();
const action = await decodeAction(formData);
try {
// Wait for any mutations
await action();
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
}
renderApp(res, null);
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
} else {
args = await decodeReply(req.body);
}
const result = action.apply(null, args);
const {pipe} = renderToPipeableStream(result, {});
pipe(res);
});
app.get('/todos', function (req, res) {

View File

@ -7,11 +7,8 @@ import {Counter as Counter2} from './Counter2.js';
import ShowMore from './ShowMore.js';
import Button from './Button.js';
import Form from './Form.js';
import {like, greet} from './actions.js';
import {getServerState} from './ServerState.js';
import {like} from './actions.js';
export default async function App() {
const res = await fetch('http://localhost:3001/todos');
@ -25,7 +22,7 @@ export default async function App() {
</head>
<body>
<Container>
<h1>{getServerState()}</h1>
<h1>Hello, world</h1>
<Counter />
<Counter2 />
<ul>
@ -36,7 +33,6 @@ export default async function App() {
<ShowMore>
<p>Lorem ipsum</p>
</ShowMore>
<Form action={greet} />
<div>
<Button action={like}>Like</Button>
</div>

View File

@ -1,26 +1,25 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
function ButtonDisabledWhilePending({action, children}) {
const {pending} = useFormStatus();
export default function Button({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
<button disabled={pending} formAction={action}>
<button
disabled={isPending}
onClick={async () => {
setIsPending(true);
try {
const result = await action();
console.log(result);
} catch (error) {
console.error(error);
} finally {
setIsPending(false);
}
}}>
{children}
</button>
);
}
export default function Button({action, children}) {
return (
<ErrorBoundary>
<form>
<ButtonDisabledWhilePending action={action}>
{children}
</ButtonDisabledWhilePending>
</form>
</ErrorBoundary>
);
}

View File

@ -1,16 +0,0 @@
'use client';
import * as React from 'react';
export default class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <div>Caught an error: {this.state.error.message}</div>;
}
return this.props.children;
}
}

View File

@ -1,29 +0,0 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
function Status() {
const {pending} = useFormStatus();
return pending ? 'Saving...' : null;
}
export default function Form({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
<ErrorBoundary>
<form action={action}>
<label>
Name: <input name="name" />
</label>
<label>
File: <input type="file" name="file" />
</label>
<button>Say Hi</button>
<Status />
</form>
</ErrorBoundary>
);
}

View File

@ -1,9 +0,0 @@
let serverState = 'Hello World';
export function setServerState(message) {
serverState = message;
}
export function getServerState() {
return serverState;
}

View File

@ -1,20 +1,5 @@
'use server';
import {setServerState} from './ServerState.js';
export async function like() {
setServerState('Liked!');
return new Promise((resolve, reject) => resolve('Liked'));
}
export async function greet(formData) {
const name = formData.get('name') || 'you';
setServerState('Hi ' + name);
const file = formData.get('file');
if (file) {
return `Ok, ${name}, here is ${file.name}:
${(await file.text()).toUpperCase()}
`;
}
return 'Hi ' + name + '!';
}

View File

@ -1,29 +1,11 @@
import * as React from 'react';
import {use, Suspense, useState, startTransition} from 'react';
import {Suspense} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
import './style.css';
let updateRoot;
async function callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
const {returnValue, root} = await createFromFetch(response, {callServer});
// Refresh the tree with the new RSC payload.
startTransition(() => {
updateRoot(root);
});
return returnValue;
}
let data = createFromFetch(
fetch('/', {
headers: {
@ -31,14 +13,21 @@ let data = createFromFetch(
},
}),
{
callServer,
async callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
return createFromFetch(response);
},
}
);
function Shell({data}) {
const [root, setRoot] = useState(use(data));
updateRoot = setRoot;
return root;
}
ReactDOM.hydrateRoot(document, <Shell data={data} />);
// TODO: This transition shouldn't really be necessary but it is for now.
React.startTransition(() => {
ReactDOM.hydrateRoot(document, data);
});

View File

@ -65,8 +65,8 @@
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^3.0.1",
"filesize": "^6.0.1",
"flow-bin": "^0.205.1",
"flow-remove-types": "^2.205.1",
"flow-bin": "^0.202.0",
"flow-remove-types": "^2.202.0",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"google-closure-compiler": "^20230206.0.0",
@ -82,6 +82,7 @@
"minimist": "^1.2.3",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"pacote": "^10.3.0",
"prettier": "2.8.3",
"pretty-format": "^29.4.1",
"prop-types": "^15.6.2",
@ -92,7 +93,6 @@
"rollup-plugin-prettier": "^3.0.0",
"rollup-plugin-strip-banner": "^3.0.0",
"semver": "^7.1.1",
"signedsource": "^2.0.0",
"targz": "^1.0.1",
"through2": "^3.0.1",
"tmp": "^0.1.0",
@ -102,7 +102,7 @@
"yargs": "^15.3.1"
},
"devEngines": {
"node": "16.x || 18.x || 19.x || 20.x"
"node": "16.x || 18.x || 19.x"
},
"jest": {
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
@ -117,7 +117,7 @@
"lint": "node ./scripts/tasks/eslint.js",
"lint-build": "node ./scripts/rollup/validate/index.js",
"extract-errors": "node scripts/error-codes/extract-errors.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
"debug-test": "yarn test --deprecated 'yarn test --debug'",
"test": "node ./scripts/jest/jest-cli.js",
"test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable",

View File

@ -18,7 +18,7 @@ const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps'];
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
}
// ***************************************************

View File

@ -26,7 +26,7 @@ ESLintTester.setDefaultConfig({
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
}
// ***************************************************

View File

@ -1103,7 +1103,7 @@ export default {
extraWarning =
` You can also do a functional update '${
setStateRecommendation.setter
}(${setStateRecommendation.missingDep.slice(
}(${setStateRecommendation.missingDep.substring(
0,
1,
)} => ...)' if you only need '${

View File

@ -12,40 +12,41 @@ import enqueueTask from './enqueueTask';
export {act} from './internalAct';
function assertYieldsWereCleared(caller) {
const actualYields = SchedulerMock.unstable_clearLog();
function assertYieldsWereCleared(Scheduler) {
const actualYields = Scheduler.unstable_clearLog();
if (actualYields.length !== 0) {
const error = Error(
'The event log is not empty. Call assertLog(...) first.',
);
Error.captureStackTrace(error, caller);
Error.captureStackTrace(error, assertYieldsWereCleared);
throw error;
}
}
export async function waitForMicrotasks() {
async function waitForMicrotasks() {
return new Promise(resolve => {
enqueueTask(() => resolve());
});
}
export async function waitFor(expectedLog, options) {
assertYieldsWereCleared(waitFor);
export async function waitFor(expectedLog) {
assertYieldsWereCleared(SchedulerMock);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitFor);
const stopAfter = expectedLog.length;
const actualLog = [];
do {
// Wait until end of current task/microtask.
await waitForMicrotasks();
if (SchedulerMock.unstable_hasPendingWork()) {
SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
SchedulerMock.unstable_flushNumberOfYields(
expectedLog.length - actualLog.length,
);
actualLog.push(...SchedulerMock.unstable_clearLog());
if (stopAfter > actualLog.length) {
if (expectedLog.length > actualLog.length) {
// Continue flushing until we've logged the expected number of items.
} else {
// Once we've reached the expected sequence, wait one more microtask to
@ -60,12 +61,6 @@ export async function waitFor(expectedLog, options) {
}
} while (true);
if (options && options.additionalLogsAfterAttemptingToYield) {
expectedLog = expectedLog.concat(
options.additionalLogsAfterAttemptingToYield,
);
}
if (equals(actualLog, expectedLog)) {
return;
}
@ -79,7 +74,7 @@ ${diff(expectedLog, actualLog)}
}
export async function waitForAll(expectedLog) {
assertYieldsWereCleared(waitForAll);
assertYieldsWereCleared(SchedulerMock);
// Create the error object before doing any async work, to get a better
// stack trace.
@ -110,7 +105,7 @@ ${diff(expectedLog, actualLog)}
}
export async function waitForThrow(expectedError: mixed): mixed {
assertYieldsWereCleared(waitForThrow);
assertYieldsWereCleared(SchedulerMock);
// Create the error object before doing any async work, to get a better
// stack trace.
@ -141,18 +136,10 @@ export async function waitForThrow(expectedError: mixed): mixed {
typeof expectedError === 'string' &&
typeof x === 'object' &&
x !== null &&
typeof x.message === 'string'
typeof x.message === 'string' &&
x.message.includes(expectedError)
) {
if (x.message.includes(expectedError)) {
return x;
} else {
error.message = `
Expected error was not thrown.
${diff(expectedError, x.message)}
`;
throw error;
}
return x;
}
error.message = `
Expected error was not thrown.
@ -164,40 +151,12 @@ ${diff(expectedError, x)}
} while (true);
}
// This is prefixed with `unstable_` because you should almost always try to
// avoid using it in tests. It's really only for testing a particular
// implementation detail (update starvation prevention).
export async function unstable_waitForExpired(expectedLog): mixed {
assertYieldsWereCleared(unstable_waitForExpired);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, unstable_waitForExpired);
// Wait until end of current task/microtask.
await waitForMicrotasks();
SchedulerMock.unstable_flushExpired();
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
// TODO: This name is a bit misleading currently because it will stop as soon as
// React yields for any reason, not just for a paint. I've left it this way for
// now because that's how untable_flushUntilNextPaint already worked, but maybe
// we should split these use cases into separate APIs.
export async function waitForPaint(expectedLog) {
assertYieldsWereCleared(waitForPaint);
assertYieldsWereCleared(SchedulerMock);
// Create the error object before doing any async work, to get a better
// stack trace.
@ -226,30 +185,6 @@ ${diff(expectedLog, actualLog)}
throw error;
}
export async function waitForDiscrete(expectedLog) {
assertYieldsWereCleared(waitForDiscrete);
// Create the error object before doing any async work, to get a better
// stack trace.
const error = new Error();
Error.captureStackTrace(error, waitForDiscrete);
// Wait until end of current task/microtask.
await waitForMicrotasks();
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {
return;
}
error.message = `
Expected sequence of events did not occur.
${diff(expectedLog, actualLog)}
`;
throw error;
}
export function assertLog(expectedLog) {
const actualLog = SchedulerMock.unstable_clearLog();
if (equals(actualLog, expectedLog)) {

View File

@ -35,7 +35,7 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
);
}
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
// $FlowFixMe: Flow doesn't know about global Jest object
if (!jest.isMockFunction(setTimeout)) {
throw Error(
"This version of `act` requires Jest's timer mocks " +
@ -70,7 +70,7 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
// Wait until end of current task/microtask.
await waitForMicrotasks();
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
// $FlowFixMe: Flow doesn't know about global Jest object
if (jest.isEnvironmentTornDown()) {
error.message =
'The Jest environment was torn down before `act` completed. This ' +
@ -79,20 +79,8 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
}
if (!Scheduler.unstable_hasPendingWork()) {
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
const j = jest;
if (j.getTimerCount() > 0) {
// There's a pending timer. Flush it now. We only do this in order to
// force Suspense fallbacks to display; the fact that it's a timer
// is an implementation detail. If there are other timers scheduled,
// those will also fire now, too, which is not ideal. (The public
// version of `act` doesn't do this.) For this reason, we should try
// to avoid using timers in our internal tests.
j.runOnlyPendingTimers();
// If a committing a fallback triggers another update, it might not
// get scheduled until a microtask. So wait one more time.
await waitForMicrotasks();
}
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
if (Scheduler.unstable_hasPendingWork()) {
// Committing a fallback scheduled additional work. Continue flushing.
} else {

View File

@ -238,13 +238,13 @@ function applyTextProps(instance, props, prevProps = {}) {
}
}
export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence';
export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberConfigWithNoResources';
export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
export function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
@ -346,10 +346,6 @@ export function getCurrentEventPriority() {
return DefaultEventPriority;
}
export function shouldAttemptEagerTransition() {
return false;
}
// The ART renderer is secondary to the React DOM renderer.
export const isPrimaryRenderer = false;
@ -479,5 +475,11 @@ export function suspendInstance(type, props) {}
export function waitForCommitToBeReady() {
return null;
}
// eslint-disable-next-line no-undef
export function prepareRendererToRender(container: Container): void {
// noop
}
export const NotPendingTransition = null;
export function resetRendererAfterRender(): void {
// noop
}

View File

@ -33,16 +33,12 @@ const ReactTestRenderer = require('react-test-renderer');
// Isolate the noop renderer
jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const Scheduler = require('scheduler');
let Group;
let Shape;
let Surface;
let TestComponent;
let waitFor;
const Missing = {};
function testDOMNodeStructure(domNode, expectedStructure) {
@ -80,8 +76,6 @@ describe('ReactART', () => {
Shape = ReactART.Shape;
Surface = ReactART.Surface;
({waitFor} = require('internal-test-utils'));
TestComponent = class extends React.Component {
group = React.createRef();
@ -363,58 +357,6 @@ describe('ReactART', () => {
doClick(instance);
expect(onClick2).toBeCalled();
});
// @gate !enableSyncDefaultUpdates
it('can concurrently render with a "primary" renderer while sharing context', async () => {
const CurrentRendererContext = React.createContext(null);
function Yield(props) {
Scheduler.log(props.value);
return null;
}
let ops = [];
function LogCurrentRenderer() {
return (
<CurrentRendererContext.Consumer>
{currentRenderer => {
ops.push(currentRenderer);
return null;
}}
</CurrentRendererContext.Consumer>
);
}
// Using test renderer instead of the DOM renderer here because async
// testing APIs for the DOM renderer don't exist.
ReactNoop.render(
<CurrentRendererContext.Provider value="Test">
<Yield value="A" />
<Yield value="B" />
<LogCurrentRenderer />
<Yield value="C" />
</CurrentRendererContext.Provider>,
);
await waitFor(['A']);
ReactDOM.render(
<Surface>
<LogCurrentRenderer />
<CurrentRendererContext.Provider value="ART">
<LogCurrentRenderer />
</CurrentRendererContext.Provider>
</Surface>,
container,
);
expect(ops).toEqual([null, 'ART']);
ops = [];
await waitFor(['B', 'C']);
expect(ops).toEqual(['Test']);
});
});
describe('ReactARTComponents', () => {

View File

@ -21,7 +21,6 @@ let textResourceShouldFail;
let waitForAll;
let assertLog;
let waitForThrow;
let act;
describe('ReactCache', () => {
beforeEach(() => {
@ -41,7 +40,6 @@ describe('ReactCache', () => {
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitForThrow = InternalTestUtils.waitForThrow;
act = InternalTestUtils.act;
TextResource = createResource(
([text, ms = 0]) => {
@ -147,14 +145,11 @@ describe('ReactCache', () => {
await waitForAll(['Suspend! [Hi]', 'Loading...']);
textResourceShouldFail = true;
let error;
try {
await act(() => jest.advanceTimersByTime(100));
} catch (e) {
error = e;
}
expect(error.message).toMatch('Failed to load: Hi');
assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
jest.advanceTimersByTime(100);
assertLog(['Promise rejected [Hi]']);
await waitForThrow('Failed to load: Hi');
assertLog(['Error! [Hi]', 'Error! [Hi]']);
// Should throw again on a subsequent read
root.update(<App />);
@ -222,8 +217,9 @@ describe('ReactCache', () => {
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]']);
await act(() => jest.advanceTimersByTime(100));
assertLog(['Promise resolved [3]', 1, 2, 3]);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [3]']);
await waitForAll([1, 2, 3]);
expect(root).toMatchRenderedOutput('123');
@ -238,17 +234,13 @@ describe('ReactCache', () => {
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [4]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
5,
]);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [4]']);
await waitForAll([1, 4, 'Suspend! [5]', 'Loading...']);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [5]']);
await waitForAll([1, 4, 5]);
expect(root).toMatchRenderedOutput('145');
@ -270,18 +262,13 @@ describe('ReactCache', () => {
'Suspend! [2]',
'Loading...',
]);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [2]']);
await waitForAll([1, 2, 'Suspend! [3]', 'Loading...']);
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [2]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
3,
]);
jest.advanceTimersByTime(100);
assertLog(['Promise resolved [3]']);
await waitForAll([1, 2, 3]);
expect(root).toMatchRenderedOutput('123');
});
@ -304,8 +291,9 @@ describe('ReactCache', () => {
await waitForAll(['Loading...']);
await act(() => jest.advanceTimersByTime(1000));
assertLog(['Promise resolved [B]', 'Promise resolved [A]', 'Result']);
jest.advanceTimersByTime(1000);
assertLog(['Promise resolved [B]', 'Promise resolved [A]']);
await waitForAll(['Result']);
expect(root).toMatchRenderedOutput('Result');
});

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightClient';
export * from './src/ReactFlightClientStream';

View File

@ -13,37 +13,25 @@ import type {LazyComponent} from 'react/src/ReactLazy';
import type {
ClientReference,
ClientReferenceMetadata,
UninitializedModel,
Response,
SSRManifest,
StringDecoder,
} from './ReactFlightClientConfig';
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
import type {CallServerCallback} from './ReactFlightReplyClient';
} from './ReactFlightClientHostConfig';
import {
resolveClientReference,
preloadModule,
requireModule,
dispatchHint,
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientConfig';
parseModel,
} from './ReactFlightClientHostConfig';
import {
encodeFormAction,
knownServerReferences,
} from './ReactFlightReplyClient';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
export type {CallServerCallback};
type UninitializedModel = string;
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type JSONValue =
| number
@ -162,15 +150,15 @@ Chunk.prototype.then = function <T>(
}
};
export type Response = {
export type ResponseBase = {
_bundlerConfig: SSRManifest,
_callServer: CallServerCallback,
_chunks: Map<number, SomeChunk<any>>,
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
...
};
export type {Response};
function readChunk<T>(chunk: SomeChunk<T>): T {
// If we have resolved content, we try to initialize it first which
// might put us back into one of the other states.
@ -201,12 +189,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
}
function createPendingChunk<T>(response: Response): PendingChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(PENDING, null, null, response);
}
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(BLOCKED, null, null, response);
}
@ -214,7 +202,7 @@ function createErrorChunk<T>(
response: Response,
error: ErrorWithDigest,
): ErroredChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(ERRORED, null, error, response);
}
@ -265,7 +253,7 @@ function createResolvedModelChunk<T>(
response: Response,
value: UninitializedModel,
): ResolvedModelChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(RESOLVED_MODEL, value, null, response);
}
@ -273,7 +261,7 @@ function createResolvedModuleChunk<T>(
response: Response,
value: ClientReference<T>,
): ResolvedModuleChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
// $FlowFixMe Flow doesn't support functions as constructors
return new Chunk(RESOLVED_MODULE, value, null, response);
}
@ -509,14 +497,11 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
return callServer(metaData.id, bound.concat(args));
});
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, metaData);
return proxy;
}
function parseModelString(
export function parseModelString(
response: Response,
parentObject: Object,
key: string,
@ -530,11 +515,11 @@ function parseModelString(
switch (value[1]) {
case '$': {
// This was an escaped string value.
return value.slice(1);
return value.substring(1);
}
case 'L': {
// Lazy node
const id = parseInt(value.slice(2), 16);
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
@ -542,21 +527,21 @@ function parseModelString(
}
case '@': {
// Promise
const id = parseInt(value.slice(2), 16);
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'S': {
// Symbol
return Symbol.for(value.slice(2));
return Symbol.for(value.substring(2));
}
case 'P': {
// Server Context Provider
return getOrCreateServerContext(value.slice(2)).Provider;
return getOrCreateServerContext(value.substring(2)).Provider;
}
case 'F': {
// Server Reference
const id = parseInt(value.slice(2), 16);
const id = parseInt(value.substring(2), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@ -574,38 +559,14 @@ function parseModelString(
throw chunk.reason;
}
}
case 'I': {
// $Infinity
return Infinity;
}
case '-': {
// $-0 or $-Infinity
if (value === '$-0') {
return -0;
} else {
return -Infinity;
}
}
case 'N': {
// $NaN
return NaN;
}
case 'u': {
// matches "$undefined"
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
case 'D': {
// Date
return new Date(Date.parse(value.slice(2)));
}
case 'n': {
// BigInt
return BigInt(value.slice(2));
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.slice(1), 16);
const id = parseInt(value.substring(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@ -636,7 +597,7 @@ function parseModelString(
return value;
}
function parseModelTuple(
export function parseModelTuple(
response: Response,
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
): any {
@ -660,25 +621,17 @@ function missingCall() {
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): Response {
): ResponseBase {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
const response = {
_bundlerConfig: bundlerConfig,
_callServer: callServer !== undefined ? callServer : missingCall,
_chunks: chunks,
_partialRow: '',
_stringDecoder: (null: any),
_fromJSON: (null: any),
};
if (supportsBinaryStreams) {
response._stringDecoder = createStringDecoder();
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
function resolveModel(
export function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
@ -692,7 +645,7 @@ function resolveModel(
}
}
function resolveModule(
export function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
@ -741,7 +694,7 @@ function resolveModule(
}
type ErrorWithDigest = Error & {digest?: string};
function resolveErrorProd(
export function resolveErrorProd(
response: Response,
id: number,
digest: string,
@ -770,7 +723,7 @@ function resolveErrorProd(
}
}
function resolveErrorDev(
export function resolveErrorDev(
response: Response,
id: number,
digest: string,
@ -801,114 +754,6 @@ function resolveErrorDev(
}
}
function resolveHint(
response: Response,
code: string,
model: UninitializedModel,
): void {
const hintModel = parseModel<HintModel>(response, model);
dispatchHint(code, hintModel);
}
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.slice(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case 'H': {
const code = row[colon + 2];
resolveHint(response, code, row.slice(colon + 3));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.slice(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.slice(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function close(response: Response): void {
// In case there are any remaining unresolved chunks, they won't
// be resolved now. So we need to issue an error to those.

View File

@ -0,0 +1,33 @@
/**
* 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 StringDecoder = void;
export const supportsBinaryStreams = false;
export function createStringDecoder(): void {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}

View File

@ -0,0 +1,23 @@
/**
* 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 {ResponseBase} from './ReactFlightClient';
import type {StringDecoder} from './ReactFlightClientHostConfig';
export type Response = ResponseBase & {
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
};
export type UninitializedModel = string;
export function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}

View File

@ -0,0 +1,140 @@
/**
* 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 {CallServerCallback} from './ReactFlightClient';
import type {Response} from './ReactFlightClientHostConfigStream';
import type {SSRManifest} from './ReactFlightClientHostConfig';
import {
resolveModule,
resolveModel,
resolveErrorProd,
resolveErrorDev,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
} from './ReactFlightClient';
import {
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientHostConfig';
export type {Response};
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.substring(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.substring(colon + 2));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.substring(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.substring(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.substring(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.substring(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): Response {
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
// It should be inlined to one object literal but minor changes can break it.
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
const response: any = createResponseBase(bundlerConfig, callServer);
response._partialRow = '';
if (supportsBinaryStreams) {
response._stringDecoder = stringDecoder;
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
export {reportGlobalError, getRoot, close} from './ReactFlightClient';

View File

@ -7,7 +7,9 @@
* @flow
*/
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
import type {Thenable} from 'shared/ReactTypes';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {
REACT_ELEMENT_TYPE,
@ -23,10 +25,6 @@ import {
} from 'shared/ReactSerializationErrors';
import isArray from 'shared/isArray';
import type {
FulfilledThenable,
RejectedThenable,
} from '../../shared/ReactTypes';
type ReactJSONValue =
| string
@ -38,15 +36,6 @@ type ReactJSONValue =
export opaque type ServerReference<T> = T;
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();
// Serializable values
export type ReactServerValue =
// References are passed by their value
@ -82,43 +71,10 @@ function serializeSymbolReference(name: string): string {
return '$S' + name;
}
function serializeFormDataReference(id: number): string {
// Why K? F is "Function". D is "Date". What else?
return '$K' + id.toString(16);
}
function serializeNumber(number: number): string | number {
if (Number.isFinite(number)) {
if (number === 0 && 1 / number === -Infinity) {
return '$-0';
} else {
return number;
}
} else {
if (number === Infinity) {
return '$Infinity';
} else if (number === -Infinity) {
return '$-Infinity';
} else {
return '$NaN';
}
}
}
function serializeUndefined(): string {
return '$undefined';
}
function serializeDateFromDateJSON(dateJSON: string): string {
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
// We need only tack on a $D prefix.
return '$D' + dateJSON;
}
function serializeBigInt(n: bigint): string {
return '$n' + n.toString(10);
}
function escapeStringValue(value: string): string {
if (value[0] === '$') {
// We need to escape $ prefixed strings since we use those to encode
@ -131,7 +87,6 @@ function escapeStringValue(value: string): string {
export function processReply(
root: ReactServerValue,
formFieldPrefix: string,
resolve: (string | FormData) => void,
reject: (error: mixed) => void,
): void {
@ -147,16 +102,10 @@ export function processReply(
value: ReactServerValue,
): ReactJSONValue {
const parent = this;
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
const originalValue = parent[key];
if (
typeof originalValue === 'object' &&
originalValue !== value &&
!(originalValue instanceof Date)
) {
// $FlowFixMe
const originalValue = this[key];
if (typeof originalValue === 'object' && originalValue !== value) {
if (objectName(originalValue) !== 'Object') {
console.error(
'Only plain objects can be passed to Server Functions from the Client. ' +
@ -197,7 +146,7 @@ export function processReply(
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
const data: FormData = formData;
// eslint-disable-next-line react-internal/safe-string-coercion
data.append(formFieldPrefix + promiseId, partJSON);
data.append('' + promiseId, partJSON);
pendingParts--;
if (pendingParts === 0) {
resolve(data);
@ -211,24 +160,6 @@ export function processReply(
);
return serializePromiseID(promiseId);
}
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
if (value instanceof FormData) {
if (formData === null) {
// Upgrade to use FormData to allow us to use rich objects as its values.
formData = new FormData();
}
const data: FormData = formData;
const refId = nextPartId++;
// Copy all the form fields with a prefix for this reference.
// These must come first in the form order because we assume that all the
// fields are available before this is referenced.
const prefix = formFieldPrefix + refId + '_';
// $FlowFixMe[prop-missing]: FormData has forEach.
value.forEach((originalValue: string | File, originalKey: string) => {
data.append(prefix + originalKey, originalValue);
});
return serializeFormDataReference(refId);
}
if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
@ -281,32 +212,18 @@ export function processReply(
}
}
// $FlowFixMe[incompatible-return]
// $FlowFixMe
return value;
}
if (typeof value === 'string') {
// TODO: Maybe too clever. If we support URL there's no similar trick.
if (value[value.length - 1] === 'Z') {
// Possibly a Date, whose toJSON automatically calls toISOString
// $FlowFixMe[incompatible-use]
const originalValue = parent[key];
if (originalValue instanceof Date) {
return serializeDateFromDateJSON(value);
}
}
return escapeStringValue(value);
}
if (typeof value === 'boolean') {
if (typeof value === 'boolean' || typeof value === 'number') {
return value;
}
if (typeof value === 'number') {
return serializeNumber(value);
}
if (typeof value === 'undefined') {
return serializeUndefined();
}
@ -322,7 +239,7 @@ export function processReply(
// The reference to this function came from the same client so we can pass it back.
const refId = nextPartId++;
// eslint-disable-next-line react-internal/safe-string-coercion
formData.set(formFieldPrefix + refId, metaDataJSON);
formData.set('' + refId, metaDataJSON);
return serializeServerReferenceID(refId);
}
throw new Error(
@ -332,13 +249,13 @@ export function processReply(
}
if (typeof value === 'symbol') {
// $FlowFixMe[incompatible-type] `description` might be undefined
// $FlowFixMe `description` might be undefined
const name: string = value.description;
if (Symbol.for(name) !== value) {
throw new Error(
'Only global symbols received from Symbol.for(...) can be passed to Server Functions. ' +
`The symbol Symbol.for(${
// $FlowFixMe[incompatible-type] `description` might be undefined
// $FlowFixMe `description` might be undefined
value.description
}) cannot be found among global symbols.`,
);
@ -347,7 +264,9 @@ export function processReply(
}
if (typeof value === 'bigint') {
return serializeBigInt(value);
throw new Error(
`BigInt (${value}) is not yet supported as an argument to a Server Function.`,
);
}
throw new Error(
@ -362,112 +281,10 @@ export function processReply(
resolve(json);
} else {
// Otherwise, we use FormData to let us stream in the result.
formData.set(formFieldPrefix + '0', json);
formData.set('0', json);
if (pendingParts === 0) {
// $FlowFixMe[incompatible-call] this has already been refined.
resolve(formData);
}
}
}
const boundCache: WeakMap<
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
Thenable<FormData>,
> = new WeakMap();
function encodeFormData(reference: any): Thenable<FormData> {
let resolve, reject;
// We need to have a handle on the thenable so that we can synchronously set
// its status from processReply, when it can complete synchronously.
const thenable: Thenable<FormData> = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
processReply(
reference,
'',
(body: string | FormData) => {
if (typeof body === 'string') {
const data = new FormData();
data.append('0', body);
body = data;
}
const fulfilled: FulfilledThenable<FormData> = (thenable: any);
fulfilled.status = 'fulfilled';
fulfilled.value = body;
resolve(body);
},
e => {
const rejected: RejectedThenable<FormData> = (thenable: any);
rejected.status = 'rejected';
rejected.reason = e;
reject(e);
},
);
return thenable;
}
export function encodeFormAction(
this: any => Promise<any>,
identifierPrefix: string,
): ReactCustomFormAction {
const reference = knownServerReferences.get(this);
if (!reference) {
throw new Error(
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
'This is a bug in React.',
);
}
let data: null | FormData = null;
let name;
const boundPromise = reference.bound;
if (boundPromise !== null) {
let thenable = boundCache.get(reference);
if (!thenable) {
thenable = encodeFormData(reference);
boundCache.set(reference, thenable);
}
if (thenable.status === 'rejected') {
throw thenable.reason;
} else if (thenable.status !== 'fulfilled') {
throw thenable;
}
const encodedFormData = thenable.value;
// This is hacky but we need the identifier prefix to be added to
// all fields but the suspense cache would break since we might get
// a new identifier each time. So we just append it at the end instead.
const prefixedData = new FormData();
// $FlowFixMe[prop-missing]
encodedFormData.forEach((value: string | File, key: string) => {
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
});
data = prefixedData;
// We encode the name of the prefix containing the data.
name = '$ACTION_REF_' + identifierPrefix;
} else {
// This is the simple case so we can just encode the ID.
name = '$ACTION_ID_' + reference.id;
}
return {
name: name,
method: 'POST',
encType: 'multipart/form-data',
data: data,
};
}
export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, {id: id, bound: null});
return proxy;
}

View File

@ -0,0 +1,17 @@
/**
* 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 {Thenable} from 'shared/ReactTypes';
type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();

View File

@ -213,40 +213,6 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
// @gate FIXME
it('should transport undefined object values', async () => {
function ServerComponent(props) {
return 'prop' in props
? `\`prop\` in props as '${props.prop}'`
: '`prop` not in props';
}
const ClientComponent = clientReference(ServerComponent);
const model = (
<>
<div>
Server: <ServerComponent prop={undefined} />
</div>
<div>
Client: <ClientComponent prop={undefined} />
</div>
</>
);
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
<>
<div>Server: `prop` in props as 'undefined'</div>
<div>Client: `prop` in props as 'undefined'</div>
</>,
);
});
it('can render an empty fragment', async () => {
function Empty() {
return <React.Fragment />;
@ -263,66 +229,6 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(null);
});
it('can transport weird numbers', async () => {
const nums = [0, -0, Infinity, -Infinity, NaN];
function ComponentClient({prop}) {
expect(prop).not.toBe(nums);
expect(prop).toEqual(nums);
expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
return `prop: ${prop}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={nums} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
// already checked -0 with expects above
'prop: 0,0,Infinity,-Infinity,NaN',
);
});
it('can transport BigInt', async () => {
function ComponentClient({prop}) {
return `prop: ${prop} (${typeof prop})`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={90071992547409910000n} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput(
'prop: 90071992547409910000 (bigint)',
);
});
it('can transport Date', async () => {
function ComponentClient({prop}) {
return `prop: ${prop.toISOString()}`;
}
const Component = clientReference(ComponentClient);
const model = <Component prop={new Date(1234567890123)} />;
const transport = ReactNoopFlightServer.render(model);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
});
it('can render a lazy component as a shared component on the server', async () => {
function SharedComponent({text}) {
return (
@ -530,6 +436,7 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
});
// @gate enableUseHook
it('should error if a non-serializable value is passed to a host component', async () => {
function ClientImpl({children}) {
return children;
@ -640,6 +547,7 @@ describe('ReactFlight', () => {
});
});
// @gate enableUseHook
it('should trigger the inner most error boundary inside a Client Component', async () => {
function ServerComponent() {
throw new Error('This was thrown in the Server Component.');
@ -690,39 +598,28 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
const obj = {
toJSON() {
return 123;
},
};
expect(() => {
const transport = ReactNoopFlightServer.render(<input value={obj} />);
const transport = ReactNoopFlightServer.render(
<input value={new Date()} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <input value={{toJSON: function}}>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
'Date objects are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
class MyError extends Error {
toJSON() {
return 123;
}
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<div>Womp womp: {new MyError('spaghetti')}</div>,
<div>Current date: {new Date()}</div>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^',
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Current date: {Date}</div>\n' +
' ^^^^^^',
{withoutStack: true},
);
});
@ -754,46 +651,37 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({value}) {
return <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
const transport = ReactNoopFlightServer.render(
<Client value={new Date()} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported.',
'Date objects are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
const obj = {
toJSON() {
return 123;
},
};
function ClientImpl({children}) {
return <div>{children}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client>Current date: {obj}</Client>,
<Client>Current date: {new Date()}</Client>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with toJSON methods are not supported. ' +
'Convert it manually to a simple value before passing it to props.\n' +
' <>Current date: {{toJSON: function}}</>\n' +
' ^^^^^^^^^^^^^^^^^^^^',
'Date objects are not supported.\n' +
' <>Current date: {Date}</>\n' +
' ^^^^^^',
{withoutStack: true},
);
});

View File

@ -1,12 +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 * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +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 * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +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 * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +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 * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +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 * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -23,24 +23,27 @@
// So `$$$config` looks like a global variable, but it's
// really an argument to a top-level wrapping function.
declare var $$$config: any;
declare var $$$hostConfig: any;
export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = mixed;
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
export const resolveClientReference = $$$config.resolveClientReference;
export const resolveServerReference = $$$config.resolveServerReference;
export const preloadModule = $$$config.preloadModule;
export const requireModule = $$$config.requireModule;
export const dispatchHint = $$$config.dispatchHint;
export const resolveClientReference = $$$hostConfig.resolveClientReference;
export const resolveServerReference = $$$hostConfig.resolveServerReference;
export const preloadModule = $$$hostConfig.preloadModule;
export const requireModule = $$$hostConfig.requireModule;
export opaque type Source = mixed;
export type UninitializedModel = string;
export const parseModel = $$$hostConfig.parseModel;
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
export const supportsBinaryStreams = $$$config.supportsBinaryStreams;
export const createStringDecoder = $$$config.createStringDecoder;
export const readPartialStringChunk = $$$config.readPartialStringChunk;
export const readFinalStringChunk = $$$config.readFinalStringChunk;
export const supportsBinaryStreams = $$$hostConfig.supportsBinaryStreams;
export const createStringDecoder = $$$hostConfig.createStringDecoder;
export const readPartialStringChunk = $$$hostConfig.readPartialStringChunk;
export const readFinalStringChunk = $$$hostConfig.readFinalStringChunk;

View File

@ -0,0 +1,12 @@
/**
* 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 * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

View File

@ -7,8 +7,8 @@
* @flow
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export type Response = any;
export opaque type SSRManifest = mixed;

View File

@ -0,0 +1,12 @@
/**
* 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 * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

View File

@ -0,0 +1,12 @@
/**
* 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 * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

View File

@ -0,0 +1,12 @@
/**
* 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 * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';

View File

@ -0,0 +1,12 @@
/**
* 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 * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';

View File

@ -0,0 +1,11 @@
/**
* 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 * from 'react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig';
export * from '../ReactFlightClientHostConfigNoStream';

View File

@ -0,0 +1,11 @@
/**
* 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 * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
export * from '../ReactFlightClientHostConfigNoStream';

View File

@ -51,19 +51,9 @@ type Dispatch<A> = A => void;
let primitiveStackCache: null | Map<string, Array<any>> = null;
type MemoCache = {
data: Array<Array<any>>,
index: number,
};
type FunctionComponentUpdateQueue = {
memoCache?: MemoCache | null,
};
type Hook = {
memoizedState: any,
next: Hook | null,
updateQueue: FunctionComponentUpdateQueue | null,
};
function getPrimitiveStackCache(): Map<string, Array<any>> {
@ -89,10 +79,6 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useDebugValue(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
if (typeof Dispatcher.useMemoCache === 'function') {
// This type check is for Flow only.
Dispatcher.useMemoCache(0);
}
} finally {
readHookLog = hookLog;
hookLog = [];
@ -120,13 +106,6 @@ function readContext<T>(context: ReactContext<T>): T {
return context._currentValue;
}
function use<T>(): T {
// TODO: What should this do if it receives an unresolved promise?
throw new Error(
'Support for `use` not yet implemented in react-debug-tools.',
);
}
function useContext<T>(context: ReactContext<T>): T {
hookLog.push({
primitive: 'Context',
@ -144,7 +123,7 @@ function useState<S>(
hook !== null
? hook.memoizedState
: typeof initialState === 'function'
? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
? // $FlowFixMe: Flow doesn't like mixed types
initialState()
: initialState;
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
@ -347,40 +326,7 @@ function useId(): string {
return id;
}
function useMemoCache(size: number): Array<any> {
const hook = nextHook();
let memoCache: MemoCache;
if (
hook !== null &&
hook.updateQueue !== null &&
hook.updateQueue.memoCache != null
) {
memoCache = hook.updateQueue.memoCache;
} else {
memoCache = {
data: [],
index: 0,
};
}
let data = memoCache.data[memoCache.index];
if (data === undefined) {
const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel');
data = new Array(size);
for (let i = 0; i < size; i++) {
data[i] = MEMO_CACHE_SENTINEL;
}
}
hookLog.push({
primitive: 'MemoCache',
stackError: new Error(),
value: data,
});
return data;
}
const Dispatcher: DispatcherType = {
use,
readContext,
useCacheRefresh,
useCallback,
@ -391,7 +337,6 @@ const Dispatcher: DispatcherType = {
useLayoutEffect,
useInsertionEffect,
useMemo,
useMemoCache,
useReducer,
useRef,
useState,
@ -568,10 +513,10 @@ function parseCustomHookName(functionName: void | string): string {
if (startIndex === -1) {
startIndex = 0;
}
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
if (functionName.substr(startIndex, 3) === 'use') {
startIndex += 3;
}
return functionName.slice(startIndex);
return functionName.substr(startIndex);
}
function buildTree(
@ -729,7 +674,7 @@ function handleRenderFunctionError(error: any): void {
// that the error is caused by user's code in renderFunction.
// In this case, we should wrap the original error inside a custom error
// so that devtools can give a clear message about it.
// $FlowFixMe[extra-arg]: Flow doesn't know about 2nd argument of Error constructor
// $FlowFixMe: Flow doesn't know about 2nd argument of Error constructor
const wrapperError = new Error('Error rendering inspected component', {
cause: error,
});
@ -737,7 +682,7 @@ function handleRenderFunctionError(error: any): void {
// TODO: refactor this if we ever combine the devtools and debug tools packages
wrapperError.name = 'ReactDebugToolsRenderError';
// this stage-4 proposal is not supported by all environments yet.
// $FlowFixMe[prop-missing] Flow doesn't have this type yet.
// $FlowFixMe Flow doesn't have this type yet.
wrapperError.cause = error;
throw wrapperError;
}

View File

@ -14,7 +14,6 @@ let React;
let ReactTestRenderer;
let ReactDebugTools;
let act;
let useMemoCache;
describe('ReactHooksInspectionIntegration', () => {
beforeEach(() => {
@ -23,7 +22,6 @@ describe('ReactHooksInspectionIntegration', () => {
ReactTestRenderer = require('react-test-renderer');
act = require('internal-test-utils').act;
ReactDebugTools = require('react-debug-tools');
useMemoCache = React.unstable_useMemoCache;
});
it('should inspect the current state of useState hooks', async () => {
@ -635,33 +633,6 @@ describe('ReactHooksInspectionIntegration', () => {
});
});
// @gate enableUseMemoCacheHook
it('should support useMemoCache hook', () => {
function Foo() {
const $ = useMemoCache(1);
let t0;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t0 = <div>{1}</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree.length).toEqual(1);
expect(tree[0].isStateEditable).toBe(false);
expect(tree[0].name).toBe('MemoCache');
expect(tree[0].value).toHaveLength(1);
expect(tree[0].value[0]).toEqual(<div>{1}</div>);
});
describe('useDebugValue', () => {
it('should support inspectable values for multiple custom hooks', () => {
function useLabeledValue(label) {
@ -898,7 +869,7 @@ describe('ReactHooksInspectionIntegration', () => {
const Suspense = React.Suspense;
function Foo(props) {
const [value] = React.useState(props.defaultValue.slice(0, 3));
const [value] = React.useState(props.defaultValue.substr(0, 3));
return <div>{value}</div>;
}
Foo.defaultProps = {

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-core",
"version": "4.27.8",
"version": "4.27.3",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -11,7 +11,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {installHook} from 'react-devtools-shared/src/hook';
import {initBackend} from 'react-devtools-shared/src/backend';
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
@ -39,9 +38,6 @@ type ConnectOptions = {
...
};
// Install a global variable to allow patching console early (during injection).
// This provides React Native developers with components stacks even if they don't run DevTools.
installConsoleFunctionsToWindow();
installHook(window);
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Meta Platforms, Inc. and its affiliates.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
@ -7,8 +7,10 @@
* @flow
*/
import type {ConsolePatchSettings} from 'react-devtools-shared/src/backend/types';
import {writeConsolePatchSettingsToWindow} from 'react-devtools-shared/src/backend/console';
import {
type ConsolePatchSettings,
writeConsolePatchSettingsToWindow,
} from 'react-devtools-shared/src/backend/console';
import {castBool, castBrowserTheme} from 'react-devtools-shared/src/utils';
// Note: all keys should be optional in this type, because users can use newer

View File

@ -30,7 +30,7 @@ import {
__DEBUG__,
LOCAL_STORAGE_DEFAULT_TAB_KEY,
} from 'react-devtools-shared/src/constants';
import {localStorageSetItem} from 'react-devtools-shared/src/storage';
import {localStorageSetItem} from '../../react-devtools-shared/src/storage';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
@ -167,6 +167,7 @@ function onDisconnected() {
disconnectedCallback();
}
// $FlowFixMe[missing-local-annot]
function onError({code, message}: $FlowFixMe) {
safeUnmount();

View File

@ -2,9 +2,9 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"version": "4.27.3",
"version_name": "4.27.3",
"minimum_chrome_version": "88",
"icons": {
"16": "icons/16-production.png",
"32": "icons/32-production.png",
@ -29,7 +29,9 @@
"resources": [
"main.html",
"panel.html",
"build/*.js"
"build/react_devtools_backend.js",
"build/proxy.js",
"build/installHook.js"
],
"matches": [
"<all_urls>"

View File

@ -7,7 +7,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./chrome/build/unpacked');
const START_URL = argv.url || 'https://react.dev/';
const START_URL = argv.url || 'https://reactjs.org/';
chromeLaunch(START_URL, {
args: [

View File

@ -27,7 +27,7 @@ const main = async buildId => {
const json = JSON.parse(file);
const alias = json.alias[0];
const commit = execSync('git rev-parse HEAD').toString().trim().slice(0, 7);
const commit = execSync('git rev-parse HEAD').toString().trim().substr(0, 7);
let date = new Date();
date = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;

View File

@ -2,9 +2,9 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"version": "4.27.3",
"version_name": "4.27.3",
"minimum_chrome_version": "88",
"icons": {
"16": "icons/16-production.png",
"32": "icons/32-production.png",
@ -29,7 +29,9 @@
"resources": [
"main.html",
"panel.html",
"build/*.js"
"build/react_devtools_backend.js",
"build/proxy.js",
"build/installHook.js"
],
"matches": [
"<all_urls>"

View File

@ -9,7 +9,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./edge/build/unpacked');
const START_URL = argv.url || 'https://react.dev/';
const START_URL = argv.url || 'https://reactjs.org/';
const extargs = `--load-extension=${EXTENSION_PATH}`;

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "4.27.8",
"version": "4.27.3",
"applications": {
"gecko": {
"id": "@react-devtools",
@ -30,7 +30,9 @@
"web_accessible_resources": [
"main.html",
"panel.html",
"build/*.js"
"build/react_devtools_backend.js",
"build/proxy.js",
"build/installHook.js"
],
"background": {
"scripts": [

View File

@ -8,7 +8,7 @@ const {resolve} = require('path');
const {argv} = require('yargs');
const EXTENSION_PATH = resolve('./firefox/build/unpacked');
const START_URL = argv.url || 'https://react.dev/';
const START_URL = argv.url || 'https://reactjs.org/';
const firefoxVersion = process.env.WEB_EXT_FIREFOX;

View File

@ -11,7 +11,7 @@
/* eslint-disable no-unused-vars */
type JestMockFn<TArguments: $ReadOnlyArray<any>, TReturn> = {
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
(...args: TArguments): TReturn,
/**
* An object for introspecting mock calls
@ -626,7 +626,7 @@ interface JestExpectType {
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
* class.
*/
toBeInstanceOf(cls: Class<any>): void;
toBeInstanceOf(cls: Class<*>): void;
/**
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
* nicer.
@ -815,7 +815,7 @@ type JestObjectType = {
* Returns a new, unused mock function. Optionally takes a mock
* implementation.
*/
fn<TArguments: $ReadOnlyArray<any>, TReturn>(
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
implementation?: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>,
/**

View File

@ -1,34 +1,109 @@
/**
* 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
*/
// Do not use imports or top-level requires here!
// Running module factories is intentionally delayed until we know the hook exists.
// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
// @flow strict-local
import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {initBackend} from 'react-devtools-shared/src/backend';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
'use strict';
import {COMPACT_VERSION_NAME} from './utils';
let welcomeHasInitialized = false;
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
function setup(hook: ?DevToolsHook) {
if (hook == null) {
// $FlowFixMe[missing-local-annot]
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
event.data.source !== 'react-devtools-content-script'
) {
return;
}
hook.backends.set(COMPACT_VERSION_NAME, {
Agent,
Bridge,
initBackend,
setupNativeStyleEditor,
// In some circumstances, this method is called more than once for a single welcome message.
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
//
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
//
// The simplest solution is to ignore the duplicate events.
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
//
// See https://github.com/facebook/react/issues/24162
if (welcomeHasInitialized) {
console.warn(
'React DevTools detected duplicate welcome "message" events from the content script.',
);
return;
}
welcomeHasInitialized = true;
window.removeEventListener('message', welcome);
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
}
window.addEventListener('message', welcome);
function setup(hook: any) {
if (hook == null) {
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return;
}
const Agent = require('react-devtools-shared/src/backend/agent').default;
const Bridge = require('react-devtools-shared/src/bridge').default;
const {initBackend} = require('react-devtools-shared/src/backend');
const setupNativeStyleEditor =
require('react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor').default;
const bridge = new Bridge<$FlowFixMe, $FlowFixMe>({
listen(fn) {
const listener = (event: $FlowFixMe) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'react-devtools-content-script' ||
!event.data.payload
) {
return;
}
fn(event.data.payload);
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
window.postMessage(
{
source: 'react-devtools-bridge',
payload: {event, payload},
},
'*',
transferable,
);
},
});
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
const agent = new Agent(bridge);
agent.addListener('shutdown', () => {
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
hook.emit('shutdown');
});
initBackend(hook, agent, window);
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
bridge.send('extensionBackendInitialized');
// Setup React Native style editor if a renderer like react-native-web has injected it.
if (hook.resolveRNStyle) {
setupNativeStyleEditor(
bridge,
agent,
hook.resolveRNStyle,
hook.nativeStyleEditorValidAttributes,
);
}
}

View File

@ -1,179 +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 {
DevToolsHook,
ReactRenderer,
} from 'react-devtools-shared/src/backend/types';
import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
import {COMPACT_VERSION_NAME} from './utils';
let welcomeHasInitialized = false;
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
event.data.source !== 'react-devtools-content-script'
) {
return;
}
// In some circumstances, this method is called more than once for a single welcome message.
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
//
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
//
// The simplest solution is to ignore the duplicate events.
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
//
// See https://github.com/facebook/react/issues/24162
if (welcomeHasInitialized) {
console.warn(
'React DevTools detected duplicate welcome "message" events from the content script.',
);
return;
}
welcomeHasInitialized = true;
window.removeEventListener('message', welcome);
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
}
window.addEventListener('message', welcome);
function setup(hook: ?DevToolsHook) {
// this should not happen, but Chrome can be weird sometimes
if (hook == null) {
return;
}
// register renderers that have already injected themselves.
hook.renderers.forEach(renderer => {
registerRenderer(renderer, hook);
});
// Activate and remove from required all present backends, registered within the hook
hook.backends.forEach((_, backendVersion) => {
requiredBackends.delete(backendVersion);
activateBackend(backendVersion, hook);
});
updateRequiredBackends();
// register renderers that inject themselves later.
hook.sub('renderer', ({renderer}) => {
registerRenderer(renderer, hook);
updateRequiredBackends();
});
// listen for backend installations.
hook.sub('devtools-backend-installed', version => {
activateBackend(version, hook);
updateRequiredBackends();
});
}
const requiredBackends = new Set<string>();
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
let version = renderer.reconcilerVersion || renderer.version;
if (!hasAssignedBackend(version)) {
version = COMPACT_VERSION_NAME;
}
// Check if required backend is already activated, no need to require again
if (!hook.backends.has(version)) {
requiredBackends.add(version);
}
}
function activateBackend(version: string, hook: DevToolsHook) {
const backend = hook.backends.get(version);
if (!backend) {
throw new Error(`Could not find backend for version "${version}"`);
}
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
const bridge = new Bridge({
listen(fn) {
const listener = (event: $FlowFixMe) => {
if (
event.source !== window ||
!event.data ||
event.data.source !== 'react-devtools-content-script' ||
!event.data.payload
) {
return;
}
fn(event.data.payload);
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
},
send(event: string, payload: any, transferable?: Array<any>) {
window.postMessage(
{
source: 'react-devtools-bridge',
payload: {event, payload},
},
'*',
transferable,
);
},
});
const agent = new Agent(bridge);
agent.addListener('shutdown', () => {
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
hook.emit('shutdown');
});
initBackend(hook, agent, window);
// Setup React Native style editor if a renderer like react-native-web has injected it.
if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) {
setupNativeStyleEditor(
bridge,
agent,
hook.resolveRNStyle,
hook.nativeStyleEditorValidAttributes,
);
}
// Let the frontend know that the backend has attached listeners and is ready for messages.
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
bridge.send('extensionBackendInitialized');
// this backend is activated
requiredBackends.delete(version);
}
// tell the service worker which versions of backends are needed for the current page
function updateRequiredBackends() {
if (requiredBackends.size === 0) {
return;
}
window.postMessage(
{
source: 'react-devtools-backend-manager',
payload: {
type: 'react-devtools-required-backends',
versions: Array.from(requiredBackends),
},
},
'*',
);
}

View File

@ -2,52 +2,34 @@
'use strict';
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
import {IS_FIREFOX} from './utils';
const ports = {};
async function dynamicallyInjectContentScripts() {
const contentScriptsToInject = [
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
{
id: 'renderer',
matches: ['<all_urls>'],
js: ['build/renderer.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
];
try {
// For some reason dynamically injected scripts might be already registered
// Registering them again will fail, which will result into
// __REACT_DEVTOOLS_GLOBAL_HOOK__ hook not being injected
// Not specifying ids, because Chrome throws an error
// if id of non-injected script is provided
await chrome.scripting.unregisterContentScripts();
// equivalent logic for Firefox is in prepareInjection.js
// Manifest V3 method of injecting content script
// TODO(hoxyq): migrate Firefox to V3 manifests
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
await chrome.scripting.registerContentScripts(contentScriptsToInject);
} catch (error) {
console.error(error);
}
}
if (!IS_FIREFOX) {
dynamicallyInjectContentScripts();
// Manifest V3 method of injecting content scripts (not yet supported in Firefox)
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
chrome.scripting.registerContentScripts(
[
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
],
function () {
// When the content scripts are already registered, an error will be thrown.
// It happens when the service worker process is incorrectly duplicated.
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
},
);
}
chrome.runtime.onConnect.addListener(function (port) {
@ -182,7 +164,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
chrome.runtime.onMessage.addListener((request, sender) => {
const tab = sender.tab;
// sender.tab.id from content script points to the tab that injected the content script
if (tab) {
const id = tab.id;
// This is sent from the hook content script.
@ -190,55 +171,15 @@ chrome.runtime.onMessage.addListener((request, sender) => {
if (request.hasDetectedReact) {
setIconAndPopup(request.reactBuildType, id);
} else {
const devtools = ports[id]?.devtools;
switch (request.payload?.type) {
case 'fetch-file-with-cache-complete':
case 'fetch-file-with-cache-error':
// Forward the result of fetch-in-page requests back to the extension.
devtools?.postMessage(request);
const devtools = ports[id]?.devtools;
if (devtools) {
devtools.postMessage(request);
}
break;
// This is sent from the backend manager running on a page
case 'react-devtools-required-backends':
const backendsToDownload = [];
request.payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId: id},
files: [`/build/react_devtools_backend_${version}.js`],
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
} else {
backendsToDownload.push(version);
}
});
// Request the necessary backends in the extension DevTools UI
// TODO: handle this message in main.js to build the UI
devtools?.postMessage({
payload: {
type: 'react-devtools-additional-backends',
versions: backendsToDownload,
},
});
break;
}
}
}
// sender.tab.id from devtools page may not exist, or point to the undocked devtools window
// so we use the payload to get the tab id
if (request.payload?.tabId) {
const tabId = request.payload?.tabId;
// This is sent from the devtools page when it is ready for injecting the backend
if (request.payload.type === 'react-devtools-inject-backend-manager') {
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId},
files: ['/build/backendManager.js'],
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
}
}

View File

@ -1,16 +1,8 @@
/* global chrome */
import nullthrows from 'nullthrows';
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
import {IS_FIREFOX} from '../utils';
// We run scripts on the page via the service worker (backgroud.js) for
// Manifest V3 extensions (Chrome & Edge).
// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
// In this content script we have access to DOM, but don't have access to the webpage's window,
// so we inject this inline script tag into the webpage (allowed in Manifest V2).
function injectScriptSync(src) {
let code = '';
const request = new XMLHttpRequest();
@ -29,6 +21,13 @@ function injectScriptSync(src) {
nullthrows(script.parentNode).removeChild(script);
}
function injectScriptAsync(src) {
const script = document.createElement('script');
script.src = src;
nullthrows(document.documentElement).appendChild(script);
nullthrows(script.parentNode).removeChild(script);
}
let lastDetectionResult;
// We want to detect when a renderer attaches, and notify the "background page"
@ -90,23 +89,10 @@ window.addEventListener('message', function onMessage({data, source}) {
);
}
break;
case 'react-devtools-inject-backend-manager':
if (IS_FIREFOX) {
injectScriptSync(chrome.runtime.getURL('build/backendManager.js'));
}
break;
case 'react-devtools-backend-manager':
if (IS_FIREFOX) {
data.payload?.versions?.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
injectScriptSync(
chrome.runtime.getURL(
`/build/react_devtools_backend_${version}.js`,
),
);
}
});
}
case 'react-devtools-inject-backend':
injectScriptAsync(
chrome.runtime.getURL('build/react_devtools_backend.js'),
);
break;
}
});
@ -122,20 +108,40 @@ window.addEventListener('pageshow', function ({target}) {
chrome.runtime.sendMessage(lastDetectionResult);
});
if (IS_FIREFOX) {
// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
if (
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
) {
injectScriptSync(chrome.runtime.getURL('build/renderer.js'));
}
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
switch (document.contentType) {
case 'text/html':
case 'application/xhtml+xml': {
injectScriptSync(chrome.runtime.getURL('build/installHook.js'));
break;
}
// We create a "sync" script tag to page to inject the global hook on Manifest V2 extensions.
// To comply with the new security policy in V3, we use chrome.scripting.registerContentScripts instead (see background.js).
// However, the new API only works for Chrome v102+.
// We insert a "async" script tag as a fallback for older versions.
// It has known issues if JS on the page is faster than the extension.
// Users will see a notice in components tab when that happens (see <Tree>).
// For Firefox, V3 is not ready, so sync injection is still the best approach.
const injectScript = IS_FIREFOX ? injectScriptSync : injectScriptAsync;
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access
// to the webpage's window, so in order to access front end settings
// and communicate with React, we must inject this code into the webpage
switch (document.contentType) {
case 'text/html':
case 'application/xhtml+xml': {
injectScript(chrome.runtime.getURL('build/installHook.js'));
break;
}
}
if (typeof exportFunction === 'function') {
// eslint-disable-next-line no-undef
exportFunction(
text => {
// Call clipboard.writeText from the extension content script
// (as it has the clipboardWrite permission) and return a Promise
// accessible to the webpage js code.
return new window.Promise((resolve, reject) =>
window.navigator.clipboard.writeText(text).then(resolve, reject),
);
},
window.wrappedJSObject.__REACT_DEVTOOLS_GLOBAL_HOOK__,
{defineAs: 'clipboardCopyText'},
);
}

View File

@ -5,7 +5,7 @@
let backendDisconnected: boolean = false;
let backendInitialized: boolean = false;
function sayHelloToBackendManager() {
function sayHelloToBackend() {
window.postMessage(
{
source: 'react-devtools-content-script',
@ -26,19 +26,14 @@ function handleMessageFromDevtools(message) {
}
function handleMessageFromPage(event) {
if (event.source === window && event.data) {
// This is a message from a bridge (initialized by a devtools backend)
if (event.data.source === 'react-devtools-bridge') {
backendInitialized = true;
if (
event.source === window &&
event.data &&
event.data.source === 'react-devtools-bridge'
) {
backendInitialized = true;
port.postMessage(event.data.payload);
}
// This is a message from the backend manager
if (event.data.source === 'react-devtools-backend-manager') {
chrome.runtime.sendMessage({
payload: event.data.payload,
});
}
port.postMessage(event.data.payload);
}
}
@ -68,17 +63,17 @@ port.onDisconnect.addListener(handleDisconnect);
window.addEventListener('message', handleMessageFromPage);
sayHelloToBackendManager();
sayHelloToBackend();
// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
// Because of this we need to poll the backend manager until it has been initialized.
// In the event of a page reload, the content script might be loaded before the backend is injected.
// Because of this we need to poll the backend until it has been initialized.
if (!backendInitialized) {
const intervalID = setInterval(() => {
if (backendInitialized || backendDisconnected) {
clearInterval(intervalID);
} else {
sayHelloToBackendManager();
sayHelloToBackend();
}
}, 500);
}

View File

@ -1,33 +0,0 @@
/**
* In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts.
* Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself,
* So this entry point (one of the web_accessible_resources) provides a way to eagerly inject it.
* The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early.
* The normal case (not a reload-and-profile) will not make use of this entry point though.
*
* @flow
*/
import {attach} from 'react-devtools-shared/src/backend/renderer';
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
if (
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' &&
!window.hasOwnProperty('__REACT_DEVTOOLS_ATTACH__')
) {
Object.defineProperty(
window,
'__REACT_DEVTOOLS_ATTACH__',
({
enumerable: false,
// This property needs to be configurable to allow third-party integrations
// to attach their own renderer. Note that using third-party integrations
// is not officially supported. Use at your own risk.
configurable: true,
get() {
return attach;
},
}: Object),
);
}

View File

@ -5,7 +5,8 @@ import {flushSync} from 'react-dom';
import {createRoot} from 'react-dom/client';
import Bridge from 'react-devtools-shared/src/bridge';
import Store from 'react-devtools-shared/src/devtools/store';
import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils';
import {getBrowserName, getBrowserTheme} from './utils';
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
import {
getAppendComponentStack,
@ -20,15 +21,15 @@ import {
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
import {
__DEBUG__,
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
} from 'react-devtools-shared/src/constants';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {logEvent} from 'react-devtools-shared/src/Logger';
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
'React::DevTools::supportsProfiling';
const isChrome = getBrowserName() === 'Chrome';
const isEdge = getBrowserName() === 'Edge';
// rAF never fires on devtools_page (because it's in the background)
// https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
// Since we render React elements here, we need to polyfill it with setTimeout
@ -175,10 +176,10 @@ function createPanelIfReactLoaded() {
store = new Store(bridge, {
isProfiling,
supportsReloadAndProfile: IS_CHROME || IS_EDGE,
supportsReloadAndProfile: isChrome || isEdge,
supportsProfiling,
// At this time, the timeline can only parse Chrome performance profiles.
supportsTimeline: IS_CHROME,
supportsTimeline: isChrome,
supportsTraceUpdates: true,
});
if (!isProfiling) {
@ -187,26 +188,14 @@ function createPanelIfReactLoaded() {
// Initialize the backend only once the Store has been initialized.
// Otherwise the Store may miss important initial tree op codes.
if (IS_CHROME || IS_EDGE) {
chrome.runtime.sendMessage({
source: 'react-devtools-main',
payload: {
type: 'react-devtools-inject-backend-manager',
tabId,
},
});
} else {
// Firefox does not support executing script in ExecutionWorld.MAIN from content script.
// see prepareInjection.js
chrome.devtools.inspectedWindow.eval(
`window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
function (response, evalError) {
if (evalError) {
console.error(evalError);
}
},
);
}
chrome.devtools.inspectedWindow.eval(
`window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
function (response, evalError) {
if (evalError) {
console.error(evalError);
}
},
);
const viewAttributeSourceFunction = (id, path) => {
const rendererID = store.getRendererIDForElement(id);
@ -266,7 +255,7 @@ function createPanelIfReactLoaded() {
// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
// never reaches the chrome.runtime.onMessage event listener.
let fetchFileWithCaching = null;
if (IS_CHROME) {
if (isChrome) {
const fetchFromNetworkCache = (url, resolve, reject) => {
// Debug ID allows us to avoid re-logging (potentially long) URL strings below,
// while also still associating (potentially) interleaved logs with the original request.
@ -474,8 +463,8 @@ function createPanelIfReactLoaded() {
let needsToSyncElementSelection = false;
chrome.devtools.panels.create(
IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
IS_EDGE ? 'icons/production.svg' : '',
isChrome || isEdge ? '⚛️ Components' : 'Components',
'',
'panel.html',
extensionPanel => {
extensionPanel.onShown.addListener(panel => {
@ -505,8 +494,8 @@ function createPanelIfReactLoaded() {
);
chrome.devtools.panels.create(
IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
IS_EDGE ? 'icons/production.svg' : '',
isChrome || isEdge ? '⚛️ Profiler' : 'Profiler',
'',
'panel.html',
extensionPanel => {
extensionPanel.onShown.addListener(panel => {

View File

@ -41,6 +41,3 @@ export function getBrowserTheme(): BrowserTheme {
}
}
}
export const COMPACT_VERSION_NAME = 'compact';
export const EXTENSION_CONTAINED_VERSIONS = [COMPACT_VERSION_NAME];

View File

@ -42,7 +42,7 @@ module.exports = {
},
output: {
path: __dirname + '/build',
filename: 'react_devtools_backend_compact.js',
filename: 'react_devtools_backend.js',
},
node: {
// Don't define a polyfill on window.setImmediate

View File

@ -51,12 +51,10 @@ module.exports = {
devtool: __DEV__ ? 'cheap-module-source-map' : false,
entry: {
background: './src/background.js',
backendManager: './src/backendManager.js',
main: './src/main.js',
panel: './src/panel.js',
proxy: './src/contentScripts/proxy.js',
prepareInjection: './src/contentScripts/prepareInjection.js',
renderer: './src/contentScripts/renderer.js',
installHook: './src/contentScripts/installHook.js',
},
output: {

View File

@ -354,6 +354,8 @@ yarn
./download-experimental-build.js --commit=main
```
Please note that you might need to [create a Circle CI API Token](https://circleci.com/docs/2.0/managing-api-tokens/) and set up an environment variable `CIRCLE_CI_API_TOKEN`.
## Build steps
Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
```sh

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-inline",
"version": "4.27.8",
"version": "4.27.3",
"description": "Embed react-devtools within a website",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -3,7 +3,6 @@
import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {initBackend} from 'react-devtools-shared/src/backend';
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
import {installHook} from 'react-devtools-shared/src/hook';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
@ -120,8 +119,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
}
export function initialize(contentWindow: any): void {
// Install a global variable to allow patching console early (during injection).
// This provides React Native developers with components stacks even if they don't run DevTools.
installConsoleFunctionsToWindow();
installHook(contentWindow);
}

View File

@ -1243,9 +1243,10 @@ describe('Timeline profiler', () => {
function Example() {
const setHigh = React.useState(0)[1];
const setLow = React.useState(0)[1];
const startTransition = React.useTransition()[1];
updaterFn = () => {
React.startTransition(() => {
startTransition(() => {
setLow(prevLow => prevLow + 1);
});
setHigh(prevHigh => prevHigh + 1);
@ -1264,6 +1265,24 @@ describe('Timeline profiler', () => {
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000001000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000010000000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "

View File

@ -13,7 +13,7 @@ function serializeHook(hook) {
// Remove user-specific portions of this file path.
let fileName = hook.hookSource.fileName;
const index = fileName.lastIndexOf('/react-devtools-shared/');
fileName = fileName.slice(index + 1);
fileName = fileName.substring(index + 1);
let subHooks = hook.subHooks;
if (subHooks) {

View File

@ -1801,7 +1801,7 @@ describe('InspectedElement', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject, undefined, 2),
JSON.stringify(nestedObject),
);
global.mockClipboardCopy.mockReset();
@ -1811,7 +1811,7 @@ describe('InspectedElement', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject.a.b, undefined, 2),
JSON.stringify(nestedObject.a.b),
);
});
@ -1841,6 +1841,7 @@ describe('InspectedElement', () => {
xyz: 1,
},
});
// $FlowFixMe
const bigInt = BigInt(123); // eslint-disable-line no-undef
await utils.actAsync(() =>
@ -1894,7 +1895,7 @@ describe('InspectedElement', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify('123n', undefined, 2),
JSON.stringify('123n'),
);
global.mockClipboardCopy.mockReset();
@ -1904,7 +1905,7 @@ describe('InspectedElement', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
JSON.stringify({0: 100, 1: -100, 2: 0}),
);
});

View File

@ -26,7 +26,7 @@ describe('InspectedElementContext', () => {
async function read(
id: number,
path: Array<string | number> = null,
path?: Array<string | number> = null,
): Promise<Object> {
const rendererID = ((store.getRendererIDForElement(id): any): number);
const promise = backendAPI
@ -826,7 +826,7 @@ describe('InspectedElementContext', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject, undefined, 2),
JSON.stringify(nestedObject),
);
global.mockClipboardCopy.mockReset();
@ -842,7 +842,7 @@ describe('InspectedElementContext', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject.a.b, undefined, 2),
JSON.stringify(nestedObject.a.b),
);
});
@ -873,6 +873,7 @@ describe('InspectedElementContext', () => {
xyz: 1,
},
});
// $FlowFixMe
const bigInt = BigInt(123); // eslint-disable-line no-undef
act(() =>
@ -932,7 +933,7 @@ describe('InspectedElementContext', () => {
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
JSON.stringify({0: 100, 1: -100, 2: 0}),
);
});
});

View File

@ -938,10 +938,8 @@ describe('ProfilingCache', () => {
}
});
// @reactVersion = 17.0
// @reactVersion >= 18.0
it('should handle unexpectedly shallow suspense trees', () => {
// This test only runs in v17 because it's a regression test for legacy
// Suspense behavior, and the implementation details changed in v18.
const container = document.createElement('div');
utils.act(() => store.profilerStore.startProfiling());
@ -965,9 +963,17 @@ describe('ProfilingCache', () => {
2 => 0,
},
"passiveEffectDuration": null,
"priorityLevel": "Normal",
"priorityLevel": "Immediate",
"timestamp": 0,
"updaters": null,
"updaters": [
{
"displayName": "render()",
"hocDisplayNames": null,
"id": 1,
"key": null,
"type": 11,
},
],
},
]
`);

View File

@ -80,6 +80,7 @@ beforeEach(() => {
}
const originalConsoleError = console.error;
// $FlowFixMe
console.error = (...args) => {
const firstArg = args[0];
if (
@ -110,6 +111,7 @@ beforeEach(() => {
originalConsoleError.apply(console, args);
};
const originalConsoleWarn = console.warn;
// $FlowFixMe
console.warn = (...args) => {
if (shouldIgnoreConsoleErrorOrWarn(args)) {
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.

View File

@ -67,6 +67,7 @@ describe('StoreStressConcurrent', () => {
// 1. Render a normal version of [a, b, c, d, e].
let container = document.createElement('div');
// $FlowFixMe
let root = ReactDOMClient.createRoot(container);
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
expect(store).toMatchInlineSnapshot(
@ -150,6 +151,7 @@ describe('StoreStressConcurrent', () => {
for (let i = 0; i < cases.length; i++) {
// Ensure fresh mount.
container = document.createElement('div');
// $FlowFixMe
root = ReactDOMClient.createRoot(container);
// Verify mounting 'abcde'.
@ -179,6 +181,7 @@ describe('StoreStressConcurrent', () => {
// 6. Verify *updates* by reusing the container between iterations.
// There'll be no unmounting until the very end.
container = document.createElement('div');
// $FlowFixMe
root = ReactDOMClient.createRoot(container);
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
@ -246,6 +249,7 @@ describe('StoreStressConcurrent', () => {
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
// We snapshot each step once so it doesn't regress.
@ -317,6 +321,7 @@ describe('StoreStressConcurrent', () => {
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
expect(print(store)).toMatch(snapshots[i]);
@ -333,6 +338,7 @@ describe('StoreStressConcurrent', () => {
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -406,6 +412,7 @@ describe('StoreStressConcurrent', () => {
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -508,6 +515,7 @@ describe('StoreStressConcurrent', () => {
// 2. Verify check Suspense can render same steps as initial fallback content.
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -532,6 +540,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -577,6 +586,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -634,6 +644,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -683,6 +694,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -736,6 +748,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -891,6 +904,7 @@ describe('StoreStressConcurrent', () => {
const snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -914,6 +928,7 @@ describe('StoreStressConcurrent', () => {
// which is different from the snapshots above. So we take more snapshots.
const fallbackSnapshots = [];
for (let i = 0; i < steps.length; i++) {
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -1047,6 +1062,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -1098,6 +1114,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -1164,6 +1181,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -1215,6 +1233,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(
@ -1266,6 +1285,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
const root = ReactDOMClient.createRoot(container);
act(() =>
root.render(

View File

@ -10,7 +10,6 @@
import {
getDisplayName,
getDisplayNameForReactElement,
isPlainObject,
} from 'react-devtools-shared/src/utils';
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
import {
@ -271,30 +270,4 @@ describe('utils', () => {
expect(gte('10.0.0', '9.0.0')).toBe(true);
});
});
describe('isPlainObject', () => {
it('should return true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({a: 1})).toBe(true);
expect(isPlainObject({a: {b: {c: 123}}})).toBe(true);
});
it('should return false if object is a class instance', () => {
expect(isPlainObject(new (class C {})())).toBe(false);
});
it('should retun false for objects, which have not only Object in its prototype chain', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(Symbol())).toBe(false);
});
it('should retun false for primitives', () => {
expect(isPlainObject(5)).toBe(false);
expect(isPlainObject(true)).toBe(false);
});
it('should return true for objects with no prototype', () => {
expect(isPlainObject(Object.create(null))).toBe(true);
});
});
});

View File

@ -46,6 +46,7 @@ export async function actAsync(
const {act: actTestRenderer} = require('react-test-renderer');
const {act: actDOM} = require('react-dom/test-utils');
// $FlowFixMe Flow doesn't know about "await act()" yet
await actDOM(async () => {
await actTestRenderer(async () => {
await cb();
@ -54,6 +55,7 @@ export async function actAsync(
if (recursivelyFlush) {
while (jest.getTimerCount() > 0) {
// $FlowFixMe Flow doesn't know about "await act()" yet
await actDOM(async () => {
await actTestRenderer(async () => {
jest.runAllTimers();
@ -61,6 +63,7 @@ export async function actAsync(
});
}
} else {
// $FlowFixMe Flow doesn't know about "await act()" yet
await actDOM(async () => {
await actTestRenderer(async () => {
jest.runOnlyPendingTimers();

View File

@ -77,7 +77,7 @@ export function describeNativeComponentFrame(
let control;
const previousPrepareStackTrace = Error.prepareStackTrace;
// $FlowFixMe[incompatible-type] It does accept undefined.
// $FlowFixMe It does accept undefined.
Error.prepareStackTrace = undefined;
reentry = true;
@ -98,7 +98,7 @@ export function describeNativeComponentFrame(
const Fake = function () {
throw Error();
};
// $FlowFixMe[prop-missing]
// $FlowFixMe
Object.defineProperty(Fake.prototype, 'props', {
set: function () {
// We use a throwing setter instead of frozen or non-writable props

View File

@ -45,7 +45,7 @@ export function disableLogs(): void {
value: disabledLog,
writable: true,
};
// $FlowFixMe[cannot-write] Flow thinks console is immutable.
// $FlowFixMe Flow thinks console is immutable.
Object.defineProperties(console, {
info: props,
log: props,
@ -69,7 +69,7 @@ export function reenableLogs(): void {
enumerable: true,
writable: true,
};
// $FlowFixMe[cannot-write] Flow thinks console is immutable.
// $FlowFixMe Flow thinks console is immutable.
Object.defineProperties(console, {
log: {...props, value: prevLog},
info: {...props, value: prevInfo},

View File

@ -16,7 +16,6 @@ import type {RendererID} from '../types';
import type {StyleAndLayout} from './types';
export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
export default function setupNativeStyleEditor(
bridge: BackendBridge,

View File

@ -0,0 +1,17 @@
/**
* 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
*/
// This list of flags must be synced with the following file:
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
export const NoFlags = /* */ 0b000000000000000000000000000;
export const PerformedWork = /* */ 0b000000000000000000000000001;
export const Placement = /* */ 0b000000000000000000000000010;
export const DidCapture = /* */ 0b000000000000000000010000000;
export const Hydrating = /* */ 0b000000000000001000000000000;

View File

@ -25,7 +25,7 @@ import {
initialize as setupTraceUpdates,
toggleEnabled as setTraceUpdatesEnabled,
} from './views/TraceUpdates';
import {patch as patchConsole} from './console';
import {patch as patchConsole, type ConsolePatchSettings} from './console';
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@ -37,13 +37,10 @@ import type {
PathMatch,
RendererID,
RendererInterface,
ConsolePatchSettings,
} from './types';
import type {
ComponentFilter,
BrowserTheme,
} from 'react-devtools-shared/src/types';
import type {ComponentFilter} from '../types';
import {isSynchronousXHRSupported} from './utils';
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
const debug = (methodName: string, ...args: Array<string>) => {
if (__DEBUG__) {
@ -300,13 +297,7 @@ export default class Agent extends EventEmitter<{
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
const value = renderer.getSerializedElementValueByPath(id, path);
if (value != null) {
this._bridge.send('saveToClipboard', value);
} else {
console.warn(`Unable to obtain serialized value for element "${id}"`);
}
renderer.copyElementPath(id, path);
}
};

View File

@ -8,12 +8,8 @@
*/
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {
CurrentDispatcherRef,
ReactRenderer,
WorkTagMap,
ConsolePatchSettings,
} from './types';
import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types';
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
import {format, formatWithStyles} from './utils';
import {getInternalReactConstants} from './renderer';
@ -140,7 +136,7 @@ export function registerRenderer(
}
}
const consoleSettingsRef: ConsolePatchSettings = {
const consoleSettingsRef = {
appendComponentStack: false,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: false,
@ -148,6 +144,14 @@ const consoleSettingsRef: ConsolePatchSettings = {
browserTheme: 'dark',
};
export type ConsolePatchSettings = {
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean,
browserTheme: BrowserTheme,
};
// Patches console methods to append component stack for the current fiber.
// Call unpatch() to remove the injected behavior.
export function patch({
@ -409,10 +413,3 @@ export function writeConsolePatchSettingsToWindow(
settings.hideConsoleLogsInStrictMode;
window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme;
}
export function installConsoleFunctionsToWindow(): void {
window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
patchConsoleUsingWindowValues,
registerRendererWithConsole: registerRenderer,
};
}

View File

@ -11,17 +11,9 @@ import Agent from './agent';
import {attach} from './renderer';
import {attach as attachLegacy} from './legacy/renderer';
import {hasAssignedBackend} from './utils';
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
// this is the backend that is compatible with all older React versions
function isMatchingRender(version: string): boolean {
return !hasAssignedBackend(version);
}
export type InitBackend = typeof initBackend;
export function initBackend(
hook: DevToolsHook,
agent: Agent,
@ -31,7 +23,6 @@ export function initBackend(
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return () => {};
}
const subs = [
hook.sub(
'renderer-attached',
@ -65,10 +56,6 @@ export function initBackend(
];
const attachRenderer = (id: number, renderer: ReactRenderer) => {
// only attach if the renderer is compatible with the current version of the backend
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
return;
}
let rendererInterface = hook.rendererInterfaces.get(id);
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
@ -89,7 +76,8 @@ export function initBackend(
}
// Notify the DevTools frontend about new renderers.
// This includes any that were attached early (via __REACT_DEVTOOLS_ATTACH__).
// This includes any that were attached early
// (when SESSION_STORAGE_RELOAD_AND_PROFILE_KEY is set to true).
if (rendererInterface != null) {
hook.emit('renderer-attached', {
id,

View File

@ -17,10 +17,10 @@ import {
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
import {
cleanForBridge,
copyToClipboard,
copyWithDelete,
copyWithRename,
copyWithSet,
serializeToString,
} from '../utils';
import {
deletePathInObject,
@ -244,6 +244,7 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -280,6 +281,7 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -316,6 +318,7 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -347,6 +350,7 @@ export function attach(
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -697,15 +701,10 @@ export function attach(
}
}
function getSerializedElementValueByPath(
id: number,
path: Array<string | number>,
): ?string {
function copyElementPath(id: number, path: Array<string | number>): void {
const inspectedElement = inspectElementRaw(id);
if (inspectedElement !== null) {
const valueToCopy = getInObject(inspectedElement, path);
return serializeToString(valueToCopy);
copyToClipboard(getInObject(inspectedElement, path));
}
}
@ -1106,7 +1105,7 @@ export function attach(
clearErrorsForFiberID,
clearWarningsForFiberID,
cleanup,
getSerializedElementValueByPath,
copyElementPath,
deletePath,
flushInitialOperations,
getBestMatchForTrackedPath,

View File

@ -54,10 +54,7 @@ let supportsUserTiming =
let supportsUserTimingV3 = false;
if (supportsUserTiming) {
const CHECK_V3_MARK = '__v3';
const markOptions: {
detail?: mixed,
startTime?: number,
} = {};
const markOptions = ({}: {startTime?: number});
Object.defineProperty(markOptions, 'startTime', {
get: function () {
supportsUserTimingV3 = true;
@ -67,6 +64,7 @@ if (supportsUserTiming) {
});
try {
// $FlowFixMe: Flow expects the User Timing level 2 API.
performance.mark(CHECK_V3_MARK, markOptions);
} catch (error) {
// Ignore
@ -283,7 +281,7 @@ export function createProfilingHooks({
);
}
// $FlowFixMe[cannot-write] This property should not be writable outside of this function.
// $FlowFixMe This property should not be writable outside of this function.
top.duration = currentTime - top.timestamp;
if (currentTimelineData) {
@ -592,7 +590,7 @@ export function createProfilingHooks({
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// $FlowFixMe[incompatible-type]: Flow cannot handle polymorphic WeakMaps
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
const wakeableIDs: WeakMap<Wakeable, number> = new PossiblyWeakMap();
let wakeableID: number = 0;
function getWakeableID(wakeable: Wakeable): number {

View File

@ -38,13 +38,10 @@ import {
utfEncodeString,
} from 'react-devtools-shared/src/utils';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
import {
gt,
gte,
serializeToString,
} from 'react-devtools-shared/src/backend/utils';
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
import {
cleanForBridge,
copyToClipboard,
copyWithDelete,
copyWithRename,
copyWithSet,
@ -91,8 +88,18 @@ import {
MEMO_SYMBOL_STRING,
SERVER_CONTEXT_SYMBOL_STRING,
} from './ReactSymbols';
import {
DidCapture,
NoFlags,
PerformedWork,
Placement,
Hydrating,
} from './ReactFiberFlags';
import {format} from './utils';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import {
enableProfilerChangedHookIndices,
enableStyleXFeatures,
} from 'react-devtools-feature-flags';
import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
@ -377,7 +384,7 @@ export function getInternalReactConstants(version: string): {
typeof type === 'object' && type !== null ? type.$$typeof : type;
return typeof symbolOrNumber === 'symbol'
? // $FlowFixMe[incompatible-return] `toString()` doesn't match the type signature?
? // $FlowFixMe `toString()` doesn't match the type signature?
symbolOrNumber.toString()
: symbolOrNumber;
}
@ -809,7 +816,7 @@ export function attach(
name: string,
fiber: Fiber,
parentFiber: ?Fiber,
extraString: string = '',
extraString?: string = '',
): void => {
if (__DEBUG__) {
const displayName =
@ -1262,12 +1269,19 @@ export function attach(
};
// Only traverse the hooks list once, depending on what info we're returning.
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
if (enableProfilerChangedHookIndices) {
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
} else {
data.didHooksChange = didHooksChange(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
}
return data;
}
@ -1448,13 +1462,12 @@ export function attach(
return false;
}
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
function didHooksChange(prev: any, next: any): boolean {
if (prev == null || next == null) {
return null;
return false;
}
const indices = [];
let index = 0;
// We can't report anything meaningful for hooks changes.
if (
next.hasOwnProperty('baseState') &&
next.hasOwnProperty('memoizedState') &&
@ -1463,15 +1476,45 @@ export function attach(
) {
while (next !== null) {
if (didStatefulHookChange(prev, next)) {
indices.push(index);
return true;
} else {
next = next.next;
prev = prev.next;
}
next = next.next;
prev = prev.next;
index++;
}
}
return indices;
return false;
}
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
if (enableProfilerChangedHookIndices) {
if (prev == null || next == null) {
return null;
}
const indices = [];
let index = 0;
if (
next.hasOwnProperty('baseState') &&
next.hasOwnProperty('memoizedState') &&
next.hasOwnProperty('next') &&
next.hasOwnProperty('queue')
) {
while (next !== null) {
if (didStatefulHookChange(prev, next)) {
indices.push(index);
}
next = next.next;
prev = prev.next;
index++;
}
}
return indices;
}
return null;
}
function getChangedKeys(prev: any, next: any): null | Array<string> {
@ -1512,10 +1555,7 @@ export function attach(
case ForwardRef:
// For types that execute user code, we check PerformedWork effect.
// We don't reflect bailouts (either referential or sCU) in DevTools.
// TODO: This flag is a leaked implementation detail. Once we start
// releasing DevTools in lockstep with React, we should import a
// function from the reconciler instead.
const PerformedWork = 0b000000000000000000000000001;
// eslint-disable-next-line no-bitwise
return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
// so it won't get highlighted with React 16.3.0 to 16.3.2.
@ -2803,12 +2843,7 @@ export function attach(
let nextNode: Fiber = node;
do {
node = nextNode;
// TODO: This function, and these flags, are a leaked implementation
// detail. Once we start releasing DevTools in lockstep with React, we
// should import a function from the reconciler instead.
const Placement = 0b000000000000000000000000010;
const Hydrating = 0b000000000000001000000000000;
if ((node.flags & (Placement | Hydrating)) !== 0) {
if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
// This is an insertion or in-progress hydration. The nearest possible
// mounted fiber is the parent but we need to continue to figure out
// if that one is still mounted.
@ -3265,21 +3300,16 @@ export function attach(
const errors = fiberIDToErrorsMap.get(id) || new Map();
const warnings = fiberIDToWarningsMap.get(id) || new Map();
let isErrored = false;
const isErrored =
(fiber.flags & DidCapture) !== NoFlags ||
forceErrorForFiberIDs.get(id) === true;
let targetErrorBoundaryID;
if (isErrorBoundary(fiber)) {
// if the current inspected element is an error boundary,
// either that we want to use it to toggle off error state
// or that we allow to force error state on it if it's within another
// error boundary
//
// TODO: This flag is a leaked implementation detail. Once we start
// releasing DevTools in lockstep with React, we should import a function
// from the reconciler instead.
const DidCapture = 0b000000000000000000010000000;
isErrored =
(fiber.flags & DidCapture) !== 0 ||
forceErrorForFiberIDs.get(id) === true;
targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
} else {
targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
@ -3508,17 +3538,14 @@ export function attach(
}
}
function getSerializedElementValueByPath(
id: number,
path: Array<string | number>,
): ?string {
function copyElementPath(id: number, path: Array<string | number>): void {
if (isMostRecentlyInspectedElement(id)) {
const valueToCopy = getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
copyToClipboard(
getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
),
);
return serializeToString(valueToCopy);
}
}
@ -4315,7 +4342,7 @@ export function attach(
if (pseudoKey === undefined) {
throw new Error('Expected root pseudo key to be known.');
}
const name = pseudoKey.slice(0, pseudoKey.lastIndexOf(':'));
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
const counter = rootDisplayNameCounter.get(name);
if (counter === undefined) {
throw new Error('Expected counter to be known.');
@ -4461,7 +4488,7 @@ export function attach(
clearErrorsAndWarnings,
clearErrorsForFiberID,
clearWarningsForFiberID,
getSerializedElementValueByPath,
copyElementPath,
deletePath,
findNativeNodesForFiberID,
flushInitialOperations,

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