Compare commits
1 Commits
main
...
fix-rollup
| Author | SHA1 | Date |
|---|---|---|
|
|
f51ad74b45 |
|
|
@ -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
|
||||
|
|
|
|||
13
.eslintrc.js
13
.eslintrc.js
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
|
||||
# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
|
||||
|
||||
React is a JavaScript library for building user interfaces.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('=');
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
let serverState = 'Hello World';
|
||||
|
||||
export function setServerState(message) {
|
||||
serverState = message;
|
||||
}
|
||||
|
||||
export function getServerState() {
|
||||
return serverState;
|
||||
}
|
||||
|
|
@ -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 + '!';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
// ***************************************************
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
// ***************************************************
|
||||
|
|
|
|||
|
|
@ -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 '${
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export * from './src/ReactFlightClient';
|
||||
export * from './src/ReactFlightClientStream';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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__;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,3 @@ export function getBrowserTheme(): BrowserTheme {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const COMPACT_VERSION_NAME = 'compact';
|
||||
export const EXTENSION_CONTAINED_VERSIONS = [COMPACT_VERSION_NAME];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue