Compare commits
91 Commits
ty-popstat
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
4b877b6c66 | |
|
|
7bd330e0b0 | |
|
|
5309f10285 | |
|
|
d7a98a5e97 | |
|
|
2468a87358 | |
|
|
f8de255e94 | |
|
|
4bfcd02b2c | |
|
|
4cd7065665 | |
|
|
a389046a52 | |
|
|
67a05d03e3 | |
|
|
df12d7eac4 | |
|
|
7cd98ef2bc | |
|
|
b5810163e9 | |
|
|
fda1f0b902 | |
|
|
7ac5e9a602 | |
|
|
16d053d592 | |
|
|
efb381bbf9 | |
|
|
b00e27342d | |
|
|
783e7fcfa3 | |
|
|
377c5175f7 | |
|
|
aef7ce5547 | |
|
|
c10010a6a0 | |
|
|
f533cee8cb | |
|
|
2c1117a8d0 | |
|
|
fa7a447b9c | |
|
|
b7972822b5 | |
|
|
388686f291 | |
|
|
8a25302c66 | |
|
|
2c2476834a | |
|
|
fa4314841e | |
|
|
5dd90c5623 | |
|
|
559e83aebb | |
|
|
67f4fb0213 | |
|
|
8ea96ef84d | |
|
|
491aec5d61 | |
|
|
9545e4810c | |
|
|
86b0e91998 | |
|
|
b12bea62d9 | |
|
|
efbd68511d | |
|
|
18282f881d | |
|
|
540bab085d | |
|
|
6eadbe0c4a | |
|
|
ec5e9c2a75 | |
|
|
f87e97a0a6 | |
|
|
25b99efe0c | |
|
|
ed545ae3d3 | |
|
|
bf449ee74e | |
|
|
64d6be7122 | |
|
|
919620b293 | |
|
|
9ece58ebaa | |
|
|
5e5342b100 | |
|
|
9c58a0b647 | |
|
|
9ee7964302 | |
|
|
2fa6323818 | |
|
|
7ce765ec32 | |
|
|
a21d1475ff | |
|
|
36e4cbe2e9 | |
|
|
8f42196892 | |
|
|
967d46c76c | |
|
|
5d7ebb4b78 | |
|
|
fd3fb8e3c5 | |
|
|
c57a0f68a4 | |
|
|
c8369527ef | |
|
|
cc93a85332 | |
|
|
fdad813ac7 | |
|
|
e5708b3ea9 | |
|
|
d73d7d5908 | |
|
|
7f8c501f68 | |
|
|
22d5942675 | |
|
|
767f52237c | |
|
|
c826dc50de | |
|
|
cd2b79dedd | |
|
|
6d394e3d26 | |
|
|
1f248bdd71 | |
|
|
b90e8ebaa5 | |
|
|
a227bcd4f4 | |
|
|
c6db19f9cd | |
|
|
96fd2fb726 | |
|
|
d8089f2cf2 | |
|
|
1b4a0daba8 | |
|
|
b433c379d5 | |
|
|
d962f35cac | |
|
|
77d3b02e5c | |
|
|
b6006201b5 | |
|
|
da6c23a45c | |
|
|
2bfe4b246f | |
|
|
ab2385fa38 | |
|
|
39a3b72c6d | |
|
|
d121c67004 | |
|
|
7b0642bb98 | |
|
|
8256781fdf |
|
|
@ -426,7 +426,6 @@ jobs:
|
|||
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
build_and_test:
|
||||
unless: << pipeline.parameters.prerelease_commit_sha >>
|
||||
|
|
@ -605,10 +604,20 @@ workflows:
|
|||
when: << pipeline.parameters.prerelease_commit_sha >>
|
||||
jobs:
|
||||
- publish_prerelease:
|
||||
name: Publish to Next channel
|
||||
name: Publish to Canary channel
|
||||
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
||||
release_channel: stable
|
||||
dist_tag: "next"
|
||||
# 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"
|
||||
- publish_prerelease:
|
||||
name: Publish to Experimental channel
|
||||
requires:
|
||||
|
|
@ -616,7 +625,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 Next channel
|
||||
- Publish to Canary channel
|
||||
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
|
|
@ -634,10 +643,10 @@ workflows:
|
|||
- main
|
||||
jobs:
|
||||
- publish_prerelease:
|
||||
name: Publish to Next channel
|
||||
name: Publish to Canary channel
|
||||
commit_sha: << pipeline.git.revision >>
|
||||
release_channel: stable
|
||||
dist_tag: "next"
|
||||
dist_tag: "canary,next"
|
||||
- publish_prerelease:
|
||||
name: Publish to Experimental channel
|
||||
requires:
|
||||
|
|
@ -645,7 +654,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 Next channel
|
||||
- Publish to Canary channel
|
||||
commit_sha: << pipeline.git.revision >>
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
|
|
|
|||
10
.eslintrc.js
10
.eslintrc.js
|
|
@ -236,7 +236,14 @@ module.exports = {
|
|||
'no-inner-declarations': [ERROR, 'functions'],
|
||||
'no-multi-spaces': ERROR,
|
||||
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
|
||||
'no-restricted-syntax': [ERROR, 'WithStatement'],
|
||||
'no-restricted-syntax': [
|
||||
ERROR,
|
||||
'WithStatement',
|
||||
{
|
||||
selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]',
|
||||
message: 'Prefer string.slice() over .substring() and .substr().',
|
||||
},
|
||||
],
|
||||
'no-shadow': ERROR,
|
||||
'no-unused-vars': [ERROR, {args: 'none'}],
|
||||
'no-use-before-define': OFF,
|
||||
|
|
@ -409,7 +416,6 @@ module.exports = {
|
|||
{
|
||||
files: [
|
||||
'packages/react-native-renderer/**/*.js',
|
||||
'packages/react-server-native-relay/**/*.js',
|
||||
],
|
||||
globals: {
|
||||
nativeFabricUIManager: 'readonly',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
name: Commit Artifacts for Facebook WWW and fbsource
|
||||
name: Commit Artifacts for Meta WWW and fbsource
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, meta-www, meta-fbsource]
|
||||
|
||||
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:
|
||||
|
|
@ -168,6 +177,7 @@ 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
|
||||
|
|
@ -204,6 +214,7 @@ 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:
|
||||
|
|
@ -217,7 +228,16 @@ 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.0.0
|
||||
// 18.3.0
|
||||
//
|
||||
// The @next channel appends additional information, with the scheme
|
||||
// The @canary channel appends additional information, with the scheme
|
||||
// <version>-<label>-<commit_sha>, e.g.:
|
||||
//
|
||||
// 18.0.0-alpha-a1c2d3e4
|
||||
// 18.3.0-canary-a1c2d3e4
|
||||
//
|
||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||
//
|
||||
|
|
@ -20,9 +20,13 @@
|
|||
|
||||
const ReactVersion = '18.3.0';
|
||||
|
||||
// The label used by the @next channel. Represents the upcoming release's
|
||||
// stability. Could be "alpha", "beta", "rc", etc.
|
||||
const nextChannelLabel = 'next';
|
||||
// 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';
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '5.0.0',
|
||||
|
|
@ -40,14 +44,14 @@ const stablePackages = {
|
|||
scheduler: '0.24.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @next or @latest channel, only
|
||||
// These packages do not exist in the @canary 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,
|
||||
nextChannelLabel,
|
||||
canaryChannelLabel,
|
||||
stablePackages,
|
||||
experimentalPackages,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class App extends PureComponent {
|
|||
}
|
||||
const multiplier = input.length !== 0 ? input.length : 1;
|
||||
const complexity =
|
||||
(parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
|
||||
(parseInt(window.location.search.slice(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@next/umd/react-cache.development.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/react-cache@canary/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@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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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,7 +11,7 @@ import semver from 'semver';
|
|||
|
||||
function parseQuery(qstr) {
|
||||
var query = {};
|
||||
var a = qstr.substr(1).split('&');
|
||||
var a = qstr.slice(1).split('&');
|
||||
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
var b = a[i].split('=');
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ 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,6 +36,7 @@ const bodyParser = require('body-parser');
|
|||
const busboy = require('busboy');
|
||||
const app = express();
|
||||
const compress = require('compression');
|
||||
const {Readable} = require('node:stream');
|
||||
|
||||
app.use(compress());
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ const {readFile} = require('fs').promises;
|
|||
|
||||
const React = require('react');
|
||||
|
||||
app.get('/', async function (req, res) {
|
||||
async function renderApp(res, returnValue) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
|
|
@ -91,37 +92,74 @@ app.get('/', async function (req, res) {
|
|||
),
|
||||
React.createElement(App),
|
||||
];
|
||||
const {pipe} = renderToPipeableStream(root, moduleMap);
|
||||
// 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);
|
||||
pipe(res);
|
||||
}
|
||||
|
||||
app.get('/', async function (req, res) {
|
||||
await renderApp(res, null);
|
||||
});
|
||||
|
||||
app.post('/', bodyParser.text(), async function (req, res) {
|
||||
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
|
||||
await import('react-server-dom-webpack/server');
|
||||
const {
|
||||
renderToPipeableStream,
|
||||
decodeReply,
|
||||
decodeReplyFromBusboy,
|
||||
decodeAction,
|
||||
} = await import('react-server-dom-webpack/server');
|
||||
const serverReference = req.get('rsc-action');
|
||||
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');
|
||||
}
|
||||
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;
|
||||
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 {
|
||||
args = await decodeReply(req.body);
|
||||
// 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 result = action.apply(null, args);
|
||||
const {pipe} = renderToPipeableStream(result, {});
|
||||
pipe(res);
|
||||
});
|
||||
|
||||
app.get('/todos', function (req, res) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ import {Counter as Counter2} from './Counter2.js';
|
|||
|
||||
import ShowMore from './ShowMore.js';
|
||||
import Button from './Button.js';
|
||||
import Form from './Form.js';
|
||||
|
||||
import {like} from './actions.js';
|
||||
import {like, greet} from './actions.js';
|
||||
|
||||
import {getServerState} from './ServerState.js';
|
||||
|
||||
export default async function App() {
|
||||
const res = await fetch('http://localhost:3001/todos');
|
||||
|
|
@ -22,7 +25,7 @@ export default async function App() {
|
|||
</head>
|
||||
<body>
|
||||
<Container>
|
||||
<h1>Hello, world</h1>
|
||||
<h1>{getServerState()}</h1>
|
||||
<Counter />
|
||||
<Counter2 />
|
||||
<ul>
|
||||
|
|
@ -33,6 +36,7 @@ export default async function App() {
|
|||
<ShowMore>
|
||||
<p>Lorem ipsum</p>
|
||||
</ShowMore>
|
||||
<Form action={greet} />
|
||||
<div>
|
||||
<Button action={like}>Like</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
export default function Button({action, children}) {
|
||||
const [isPending, setIsPending] = React.useState(false);
|
||||
|
||||
function ButtonDisabledWhilePending({action, children}) {
|
||||
const {pending} = useFormStatus();
|
||||
return (
|
||||
<button
|
||||
disabled={isPending}
|
||||
onClick={async () => {
|
||||
setIsPending(true);
|
||||
try {
|
||||
const result = await action();
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
}}>
|
||||
<button disabled={pending} formAction={action}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Button({action, children}) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<form>
|
||||
<ButtonDisabledWhilePending action={action}>
|
||||
{children}
|
||||
</ButtonDisabledWhilePending>
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
'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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
let serverState = 'Hello World';
|
||||
|
||||
export function setServerState(message) {
|
||||
serverState = message;
|
||||
}
|
||||
|
||||
export function getServerState() {
|
||||
return serverState;
|
||||
}
|
||||
|
|
@ -1,5 +1,20 @@
|
|||
'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,11 +1,29 @@
|
|||
import * as React from 'react';
|
||||
import {Suspense} from 'react';
|
||||
import {use, Suspense, useState, startTransition} 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: {
|
||||
|
|
@ -13,21 +31,14 @@ let data = createFromFetch(
|
|||
},
|
||||
}),
|
||||
{
|
||||
async callServer(id, args) {
|
||||
const response = fetch('/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
'rsc-action': id,
|
||||
},
|
||||
body: await encodeReply(args),
|
||||
});
|
||||
return createFromFetch(response);
|
||||
},
|
||||
callServer,
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: This transition shouldn't really be necessary but it is for now.
|
||||
React.startTransition(() => {
|
||||
ReactDOM.hydrateRoot(document, data);
|
||||
});
|
||||
function Shell({data}) {
|
||||
const [root, setRoot] = useState(use(data));
|
||||
updateRoot = setRoot;
|
||||
return root;
|
||||
}
|
||||
|
||||
ReactDOM.hydrateRoot(document, <Shell data={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.202.0",
|
||||
"flow-remove-types": "^2.202.0",
|
||||
"flow-bin": "^0.205.1",
|
||||
"flow-remove-types": "^2.205.1",
|
||||
"glob": "^7.1.6",
|
||||
"glob-stream": "^6.1.0",
|
||||
"google-closure-compiler": "^20230206.0.0",
|
||||
|
|
@ -82,7 +82,6 @@
|
|||
"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",
|
||||
|
|
@ -93,6 +92,7 @@
|
|||
"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"
|
||||
"node": "16.x || 18.x || 19.x || 20.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 && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
|
||||
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.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.substr(leftPadding.length)).join('\n');
|
||||
return codeLines.map(line => line.slice(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.substr(leftPadding.length)).join('\n');
|
||||
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
|
||||
}
|
||||
|
||||
// ***************************************************
|
||||
|
|
|
|||
|
|
@ -1103,7 +1103,7 @@ export default {
|
|||
extraWarning =
|
||||
` You can also do a functional update '${
|
||||
setStateRecommendation.setter
|
||||
}(${setStateRecommendation.missingDep.substring(
|
||||
}(${setStateRecommendation.missingDep.slice(
|
||||
0,
|
||||
1,
|
||||
)} => ...)' if you only need '${
|
||||
|
|
|
|||
|
|
@ -479,11 +479,5 @@ export function suspendInstance(type, props) {}
|
|||
export function waitForCommitToBeReady() {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
export function prepareRendererToRender(container: Container): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function resetRendererAfterRender(): void {
|
||||
// noop
|
||||
}
|
||||
export const NotPendingTransition = null;
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export * from './src/ReactFlightClientStream';
|
||||
export * from './src/ReactFlightClient';
|
||||
|
|
|
|||
|
|
@ -13,25 +13,37 @@ 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';
|
||||
|
||||
import {
|
||||
resolveClientReference,
|
||||
preloadModule,
|
||||
requireModule,
|
||||
parseModel,
|
||||
dispatchHint,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
supportsBinaryStreams,
|
||||
createStringDecoder,
|
||||
} from './ReactFlightClientConfig';
|
||||
|
||||
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
||||
import {
|
||||
encodeFormAction,
|
||||
knownServerReferences,
|
||||
} from './ReactFlightReplyClient';
|
||||
|
||||
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
|
||||
|
||||
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
|
||||
export type {CallServerCallback};
|
||||
|
||||
type UninitializedModel = string;
|
||||
|
||||
export type JSONValue =
|
||||
| number
|
||||
|
|
@ -150,15 +162,15 @@ Chunk.prototype.then = function <T>(
|
|||
}
|
||||
};
|
||||
|
||||
export type ResponseBase = {
|
||||
export type Response = {
|
||||
_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.
|
||||
|
|
@ -497,11 +509,14 @@ 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;
|
||||
}
|
||||
|
||||
export function parseModelString(
|
||||
function parseModelString(
|
||||
response: Response,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
|
|
@ -515,11 +530,11 @@ export function parseModelString(
|
|||
switch (value[1]) {
|
||||
case '$': {
|
||||
// This was an escaped string value.
|
||||
return value.substring(1);
|
||||
return value.slice(1);
|
||||
}
|
||||
case 'L': {
|
||||
// Lazy node
|
||||
const id = parseInt(value.substring(2), 16);
|
||||
const id = parseInt(value.slice(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.
|
||||
|
|
@ -527,21 +542,21 @@ export function parseModelString(
|
|||
}
|
||||
case '@': {
|
||||
// Promise
|
||||
const id = parseInt(value.substring(2), 16);
|
||||
const id = parseInt(value.slice(2), 16);
|
||||
const chunk = getChunk(response, id);
|
||||
return chunk;
|
||||
}
|
||||
case 'S': {
|
||||
// Symbol
|
||||
return Symbol.for(value.substring(2));
|
||||
return Symbol.for(value.slice(2));
|
||||
}
|
||||
case 'P': {
|
||||
// Server Context Provider
|
||||
return getOrCreateServerContext(value.substring(2)).Provider;
|
||||
return getOrCreateServerContext(value.slice(2)).Provider;
|
||||
}
|
||||
case 'F': {
|
||||
// Server Reference
|
||||
const id = parseInt(value.substring(2), 16);
|
||||
const id = parseInt(value.slice(2), 16);
|
||||
const chunk = getChunk(response, id);
|
||||
switch (chunk.status) {
|
||||
case RESOLVED_MODEL:
|
||||
|
|
@ -559,18 +574,38 @@ export 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.substring(2));
|
||||
return BigInt(value.slice(2));
|
||||
}
|
||||
default: {
|
||||
// We assume that anything else is a reference ID.
|
||||
const id = parseInt(value.substring(1), 16);
|
||||
const id = parseInt(value.slice(1), 16);
|
||||
const chunk = getChunk(response, id);
|
||||
switch (chunk.status) {
|
||||
case RESOLVED_MODEL:
|
||||
|
|
@ -601,7 +636,7 @@ export function parseModelString(
|
|||
return value;
|
||||
}
|
||||
|
||||
export function parseModelTuple(
|
||||
function parseModelTuple(
|
||||
response: Response,
|
||||
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
||||
): any {
|
||||
|
|
@ -625,17 +660,25 @@ function missingCall() {
|
|||
export function createResponse(
|
||||
bundlerConfig: SSRManifest,
|
||||
callServer: void | CallServerCallback,
|
||||
): ResponseBase {
|
||||
): Response {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
const response = {
|
||||
const response: 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;
|
||||
}
|
||||
|
||||
export function resolveModel(
|
||||
function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
|
|
@ -649,7 +692,7 @@ export function resolveModel(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveModule(
|
||||
function resolveModule(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
|
|
@ -698,7 +741,7 @@ export function resolveModule(
|
|||
}
|
||||
|
||||
type ErrorWithDigest = Error & {digest?: string};
|
||||
export function resolveErrorProd(
|
||||
function resolveErrorProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
digest: string,
|
||||
|
|
@ -727,7 +770,7 @@ export function resolveErrorProd(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveErrorDev(
|
||||
function resolveErrorDev(
|
||||
response: Response,
|
||||
id: number,
|
||||
digest: string,
|
||||
|
|
@ -758,6 +801,114 @@ export 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.
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export type 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');
|
||||
}
|
||||
|
|
@ -1,23 +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 {ResponseBase} from './ReactFlightClient';
|
||||
import type {StringDecoder} from './ReactFlightClientConfig';
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,140 +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 {CallServerCallback} from './ReactFlightClient';
|
||||
import type {Response} from './ReactFlightClientConfigStream';
|
||||
import type {SSRManifest} from './ReactFlightClientConfig';
|
||||
|
||||
import {
|
||||
resolveModule,
|
||||
resolveModel,
|
||||
resolveErrorProd,
|
||||
resolveErrorDev,
|
||||
createResponse as createResponseBase,
|
||||
parseModelString,
|
||||
parseModelTuple,
|
||||
} from './ReactFlightClient';
|
||||
|
||||
import {
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
supportsBinaryStreams,
|
||||
createStringDecoder,
|
||||
} from './ReactFlightClientConfig';
|
||||
|
||||
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,9 +7,7 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
||||
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
REACT_ELEMENT_TYPE,
|
||||
|
|
@ -25,6 +23,10 @@ import {
|
|||
} from 'shared/ReactSerializationErrors';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
import type {
|
||||
FulfilledThenable,
|
||||
RejectedThenable,
|
||||
} from '../../shared/ReactTypes';
|
||||
|
||||
type ReactJSONValue =
|
||||
| string
|
||||
|
|
@ -36,6 +38,15 @@ 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
|
||||
|
|
@ -71,10 +82,39 @@ 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);
|
||||
}
|
||||
|
|
@ -91,6 +131,7 @@ function escapeStringValue(value: string): string {
|
|||
|
||||
export function processReply(
|
||||
root: ReactServerValue,
|
||||
formFieldPrefix: string,
|
||||
resolve: (string | FormData) => void,
|
||||
reject: (error: mixed) => void,
|
||||
): void {
|
||||
|
|
@ -106,10 +147,16 @@ 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 = this[key];
|
||||
if (typeof originalValue === 'object' && originalValue !== value) {
|
||||
const originalValue = parent[key];
|
||||
if (
|
||||
typeof originalValue === 'object' &&
|
||||
originalValue !== value &&
|
||||
!(originalValue instanceof Date)
|
||||
) {
|
||||
if (objectName(originalValue) !== 'Object') {
|
||||
console.error(
|
||||
'Only plain objects can be passed to Server Functions from the Client. ' +
|
||||
|
|
@ -150,7 +197,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('' + promiseId, partJSON);
|
||||
data.append(formFieldPrefix + promiseId, partJSON);
|
||||
pendingParts--;
|
||||
if (pendingParts === 0) {
|
||||
resolve(data);
|
||||
|
|
@ -164,6 +211,24 @@ 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) {
|
||||
|
|
@ -221,13 +286,27 @@ export function processReply(
|
|||
}
|
||||
|
||||
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' || typeof value === 'number') {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return serializeNumber(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return serializeUndefined();
|
||||
}
|
||||
|
|
@ -243,7 +322,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('' + refId, metaDataJSON);
|
||||
formData.set(formFieldPrefix + refId, metaDataJSON);
|
||||
return serializeServerReferenceID(refId);
|
||||
}
|
||||
throw new Error(
|
||||
|
|
@ -283,10 +362,112 @@ export function processReply(
|
|||
resolve(json);
|
||||
} else {
|
||||
// Otherwise, we use FormData to let us stream in the result.
|
||||
formData.set('0', json);
|
||||
formData.set(formFieldPrefix + '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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +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 {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
type ServerReferenceId = any;
|
||||
|
||||
export const knownServerReferences: WeakMap<
|
||||
Function,
|
||||
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
||||
> = new WeakMap();
|
||||
|
|
@ -263,6 +263,30 @@ 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})`;
|
||||
|
|
@ -282,6 +306,23 @@ describe('ReactFlight', () => {
|
|||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
|
|
@ -489,7 +530,6 @@ 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;
|
||||
|
|
@ -600,7 +640,6 @@ 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.');
|
||||
|
|
@ -651,28 +690,39 @@ 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={new Date()} />,
|
||||
);
|
||||
const transport = ReactNoopFlightServer.render(<input value={obj} />);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.',
|
||||
'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' +
|
||||
' ^^^^^^^^^^^^^^^^^^^^',
|
||||
{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>Current date: {new Date()}</div>,
|
||||
<div>Womp womp: {new MyError('spaghetti')}</div>,
|
||||
);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Current date: {Date}</div>\n' +
|
||||
' ^^^^^^',
|
||||
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Womp womp: {Error}</div>\n' +
|
||||
' ^^^^^^^',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
|
@ -704,37 +754,46 @@ 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={new Date()} />,
|
||||
);
|
||||
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.',
|
||||
'Objects with toJSON methods 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: {new Date()}</Client>,
|
||||
<Client>Current date: {obj}</Client>,
|
||||
);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.\n' +
|
||||
' <>Current date: {Date}</>\n' +
|
||||
' ^^^^^^',
|
||||
'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' +
|
||||
' ^^^^^^^^^^^^^^^^^^^^',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
declare var $$$config: any;
|
||||
|
||||
export type Response = any;
|
||||
export opaque type SSRManifest = mixed;
|
||||
export opaque type ServerManifest = mixed;
|
||||
export opaque type ServerReferenceId = string;
|
||||
|
|
@ -35,12 +34,10 @@ 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 opaque type Source = mixed;
|
||||
|
||||
export type UninitializedModel = string;
|
||||
export const parseModel = $$$config.parseModel;
|
||||
|
||||
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
|
||||
|
||||
export const supportsBinaryStreams = $$$config.supportsBinaryStreams;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
||||
export type Response = any;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigNode';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,5 @@
|
|||
*/
|
||||
|
||||
export * from 'react-client/src/ReactFlightClientConfigNode';
|
||||
export * from 'react-client/src/ReactFlightClientConfigStream';
|
||||
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
|
||||
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-server-dom-relay/src/ReactFlightClientConfigDOMRelay';
|
||||
export * from '../ReactFlightClientConfigNoStream';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
|
|
@ -1,11 +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-server-native-relay/src/ReactFlightClientConfigNativeRelay';
|
||||
export * from '../ReactFlightClientConfigNoStream';
|
||||
|
|
@ -51,9 +51,19 @@ 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>> {
|
||||
|
|
@ -79,6 +89,10 @@ 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 = [];
|
||||
|
|
@ -106,6 +120,13 @@ 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',
|
||||
|
|
@ -326,7 +347,40 @@ 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,
|
||||
|
|
@ -337,6 +391,7 @@ const Dispatcher: DispatcherType = {
|
|||
useLayoutEffect,
|
||||
useInsertionEffect,
|
||||
useMemo,
|
||||
useMemoCache,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
|
@ -513,10 +568,10 @@ function parseCustomHookName(functionName: void | string): string {
|
|||
if (startIndex === -1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
if (functionName.substr(startIndex, 3) === 'use') {
|
||||
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
|
||||
startIndex += 3;
|
||||
}
|
||||
return functionName.substr(startIndex);
|
||||
return functionName.slice(startIndex);
|
||||
}
|
||||
|
||||
function buildTree(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ let React;
|
|||
let ReactTestRenderer;
|
||||
let ReactDebugTools;
|
||||
let act;
|
||||
let useMemoCache;
|
||||
|
||||
describe('ReactHooksInspectionIntegration', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -22,6 +23,7 @@ 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 () => {
|
||||
|
|
@ -633,6 +635,33 @@ 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) {
|
||||
|
|
@ -869,7 +898,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
|||
const Suspense = React.Suspense;
|
||||
|
||||
function Foo(props) {
|
||||
const [value] = React.useState(props.defaultValue.substr(0, 3));
|
||||
const [value] = React.useState(props.defaultValue.slice(0, 3));
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
Foo.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
* Copyright (c) Meta Platforms, 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.
|
||||
|
|
|
|||
|
|
@ -167,7 +167,6 @@ function onDisconnected() {
|
|||
disconnectedCallback();
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function onError({code, message}: $FlowFixMe) {
|
||||
safeUnmount();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"manifest_version": 3,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "4.27.4",
|
||||
"version_name": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"version_name": "4.27.8",
|
||||
"minimum_chrome_version": "102",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
|
@ -29,10 +29,7 @@
|
|||
"resources": [
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/proxy.js",
|
||||
"build/renderer.js",
|
||||
"build/installHook.js"
|
||||
"build/*.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://reactjs.org/';
|
||||
const START_URL = argv.url || 'https://react.dev/';
|
||||
|
||||
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().substr(0, 7);
|
||||
const commit = execSync('git rev-parse HEAD').toString().trim().slice(0, 7);
|
||||
|
||||
let date = new Date();
|
||||
date = `${date.toLocaleDateString()} – ${date.toLocaleTimeString()}`;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"manifest_version": 3,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
|
||||
"version": "4.27.4",
|
||||
"version_name": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"version_name": "4.27.8",
|
||||
"minimum_chrome_version": "102",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
|
@ -29,10 +29,7 @@
|
|||
"resources": [
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/proxy.js",
|
||||
"build/renderer.js",
|
||||
"build/installHook.js"
|
||||
"build/*.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://reactjs.org/';
|
||||
const START_URL = argv.url || 'https://react.dev/';
|
||||
|
||||
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.4",
|
||||
"version": "4.27.8",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "@react-devtools",
|
||||
|
|
@ -30,10 +30,7 @@
|
|||
"web_accessible_resources": [
|
||||
"main.html",
|
||||
"panel.html",
|
||||
"build/react_devtools_backend.js",
|
||||
"build/proxy.js",
|
||||
"build/renderer.js",
|
||||
"build/installHook.js"
|
||||
"build/*.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://reactjs.org/';
|
||||
const START_URL = argv.url || 'https://react.dev/';
|
||||
|
||||
const firefoxVersion = process.env.WEB_EXT_FIREFOX;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
|
||||
type JestMockFn<TArguments: $ReadOnlyArray<any>, 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<*>): void;
|
||||
toBeInstanceOf(cls: Class<any>): 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<*>, TReturn>(
|
||||
fn<TArguments: $ReadOnlyArray<any>, TReturn>(
|
||||
implementation?: (...args: TArguments) => TReturn
|
||||
): JestMockFn<TArguments, TReturn>,
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,109 +1,34 @@
|
|||
// 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
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// @flow strict-local
|
||||
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
'use strict';
|
||||
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';
|
||||
|
||||
let welcomeHasInitialized = false;
|
||||
import {COMPACT_VERSION_NAME} from './utils';
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function welcome(event: $FlowFixMe) {
|
||||
if (
|
||||
event.source !== window ||
|
||||
event.data.source !== 'react-devtools-content-script'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
||||
|
||||
// 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) {
|
||||
function setup(hook: ?DevToolsHook) {
|
||||
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.backends.set(COMPACT_VERSION_NAME, {
|
||||
Agent,
|
||||
Bridge,
|
||||
initBackend,
|
||||
setupNativeStyleEditor,
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* 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,42 +2,52 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
import {IS_FIREFOX} from './utils';
|
||||
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
|
||||
|
||||
const ports = {};
|
||||
|
||||
if (!IS_FIREFOX) {
|
||||
// equivalent logic for Firefox is in prepareInjection.js
|
||||
// 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,
|
||||
},
|
||||
{
|
||||
id: 'renderer',
|
||||
matches: ['<all_urls>'],
|
||||
js: ['build/renderer.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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
chrome.runtime.onConnect.addListener(function (port) {
|
||||
|
|
@ -172,6 +182,7 @@ 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.
|
||||
|
|
@ -179,26 +190,53 @@ 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.
|
||||
const devtools = ports[id]?.devtools;
|
||||
if (devtools) {
|
||||
devtools.postMessage(request);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else if (request.payload?.tabId) {
|
||||
}
|
||||
// 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') {
|
||||
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/react_devtools_backend.js'],
|
||||
files: ['/build/backendManager.js'],
|
||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
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} from '../utils';
|
||||
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
|
||||
|
||||
// We run scripts on the page via the service worker (backgroud.js) for
|
||||
// Manifest V3 extensions (Chrome & Edge).
|
||||
|
|
@ -90,11 +90,22 @@ window.addEventListener('message', function onMessage({data, source}) {
|
|||
);
|
||||
}
|
||||
break;
|
||||
case 'react-devtools-inject-backend':
|
||||
case 'react-devtools-inject-backend-manager':
|
||||
if (IS_FIREFOX) {
|
||||
injectScriptSync(
|
||||
chrome.runtime.getURL('build/react_devtools_backend.js'),
|
||||
);
|
||||
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`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
let backendDisconnected: boolean = false;
|
||||
let backendInitialized: boolean = false;
|
||||
|
||||
function sayHelloToBackend() {
|
||||
function sayHelloToBackendManager() {
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script',
|
||||
|
|
@ -26,14 +26,19 @@ function handleMessageFromDevtools(message) {
|
|||
}
|
||||
|
||||
function handleMessageFromPage(event) {
|
||||
if (
|
||||
event.source === window &&
|
||||
event.data &&
|
||||
event.data.source === 'react-devtools-bridge'
|
||||
) {
|
||||
backendInitialized = true;
|
||||
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;
|
||||
|
||||
port.postMessage(event.data.payload);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,17 +68,17 @@ port.onDisconnect.addListener(handleDisconnect);
|
|||
|
||||
window.addEventListener('message', handleMessageFromPage);
|
||||
|
||||
sayHelloToBackend();
|
||||
sayHelloToBackendManager();
|
||||
|
||||
// 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 is injected.
|
||||
// Because of this we need to poll the backend until it has been initialized.
|
||||
// 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.
|
||||
if (!backendInitialized) {
|
||||
const intervalID = setInterval(() => {
|
||||
if (backendInitialized || backendDisconnected) {
|
||||
clearInterval(intervalID);
|
||||
} else {
|
||||
sayHelloToBackend();
|
||||
sayHelloToBackendManager();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ function createPanelIfReactLoaded() {
|
|||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-main',
|
||||
payload: {
|
||||
type: 'react-devtools-inject-backend',
|
||||
type: 'react-devtools-inject-backend-manager',
|
||||
tabId,
|
||||
},
|
||||
});
|
||||
|
|
@ -199,7 +199,7 @@ function createPanelIfReactLoaded() {
|
|||
// 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' }, '*');`,
|
||||
`window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
|
||||
function (response, evalError) {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
|
|
|
|||
|
|
@ -41,3 +41,6 @@ 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.js',
|
||||
filename: 'react_devtools_backend_compact.js',
|
||||
},
|
||||
node: {
|
||||
// Don't define a polyfill on window.setImmediate
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ 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',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
|
|
|
|||
|
|
@ -1243,10 +1243,9 @@ describe('Timeline profiler', () => {
|
|||
function Example() {
|
||||
const setHigh = React.useState(0)[1];
|
||||
const setLow = React.useState(0)[1];
|
||||
const startTransition = React.useTransition()[1];
|
||||
|
||||
updaterFn = () => {
|
||||
startTransition(() => {
|
||||
React.startTransition(() => {
|
||||
setLow(prevLow => prevLow + 1);
|
||||
});
|
||||
setHigh(prevHigh => prevHigh + 1);
|
||||
|
|
@ -1265,24 +1264,6 @@ 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.substring(index + 1);
|
||||
fileName = fileName.slice(index + 1);
|
||||
|
||||
let subHooks = hook.subHooks;
|
||||
if (subHooks) {
|
||||
|
|
|
|||
|
|
@ -938,8 +938,10 @@ describe('ProfilingCache', () => {
|
|||
}
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
// @reactVersion = 17.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,15 +967,7 @@ describe('ProfilingCache', () => {
|
|||
"passiveEffectDuration": null,
|
||||
"priorityLevel": "Normal",
|
||||
"timestamp": 0,
|
||||
"updaters": [
|
||||
{
|
||||
"displayName": "render()",
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
"key": null,
|
||||
"type": 11,
|
||||
},
|
||||
],
|
||||
"updaters": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,17 @@ 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,
|
||||
|
|
@ -23,6 +31,7 @@ export function initBackend(
|
|||
// DevTools didn't get injected into this page (maybe b'c of the contentType).
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const subs = [
|
||||
hook.sub(
|
||||
'renderer-attached',
|
||||
|
|
@ -56,6 +65,10 @@ 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)
|
||||
|
|
|
|||
|
|
@ -244,7 +244,6 @@ export function attach(
|
|||
parentIDStack.pop();
|
||||
return result;
|
||||
} catch (err) {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
parentIDStack = [];
|
||||
throw err;
|
||||
} finally {
|
||||
|
|
@ -281,7 +280,6 @@ export function attach(
|
|||
parentIDStack.pop();
|
||||
return result;
|
||||
} catch (err) {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
parentIDStack = [];
|
||||
throw err;
|
||||
} finally {
|
||||
|
|
@ -318,7 +316,6 @@ export function attach(
|
|||
parentIDStack.pop();
|
||||
return result;
|
||||
} catch (err) {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
parentIDStack = [];
|
||||
throw err;
|
||||
} finally {
|
||||
|
|
@ -350,7 +347,6 @@ export function attach(
|
|||
|
||||
return result;
|
||||
} catch (err) {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
parentIDStack = [];
|
||||
throw err;
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ let supportsUserTiming =
|
|||
let supportsUserTimingV3 = false;
|
||||
if (supportsUserTiming) {
|
||||
const CHECK_V3_MARK = '__v3';
|
||||
const markOptions = ({}: {startTime?: number});
|
||||
const markOptions: {
|
||||
detail?: mixed,
|
||||
startTime?: number,
|
||||
} = {};
|
||||
Object.defineProperty(markOptions, 'startTime', {
|
||||
get: function () {
|
||||
supportsUserTimingV3 = true;
|
||||
|
|
@ -64,7 +67,6 @@ if (supportsUserTiming) {
|
|||
});
|
||||
|
||||
try {
|
||||
// $FlowFixMe[extra-arg]: Flow expects the User Timing level 2 API.
|
||||
performance.mark(CHECK_V3_MARK, markOptions);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
|
|
|
|||
|
|
@ -92,10 +92,7 @@ import {
|
|||
SERVER_CONTEXT_SYMBOL_STRING,
|
||||
} from './ReactSymbols';
|
||||
import {format} from './utils';
|
||||
import {
|
||||
enableProfilerChangedHookIndices,
|
||||
enableStyleXFeatures,
|
||||
} from 'react-devtools-feature-flags';
|
||||
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
|
||||
import is from 'shared/objectIs';
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import {getStyleXData} from './StyleX/utils';
|
||||
|
|
@ -1265,19 +1262,12 @@ export function attach(
|
|||
};
|
||||
|
||||
// Only traverse the hooks list once, depending on what info we're returning.
|
||||
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,
|
||||
);
|
||||
}
|
||||
const indices = getChangedHooksIndices(
|
||||
prevFiber.memoizedState,
|
||||
nextFiber.memoizedState,
|
||||
);
|
||||
data.hooks = indices;
|
||||
data.didHooksChange = indices !== null && indices.length > 0;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -1458,12 +1448,13 @@ export function attach(
|
|||
return false;
|
||||
}
|
||||
|
||||
function didHooksChange(prev: any, next: any): boolean {
|
||||
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
|
||||
if (prev == null || next == null) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can't report anything meaningful for hooks changes.
|
||||
const indices = [];
|
||||
let index = 0;
|
||||
if (
|
||||
next.hasOwnProperty('baseState') &&
|
||||
next.hasOwnProperty('memoizedState') &&
|
||||
|
|
@ -1472,45 +1463,15 @@ export function attach(
|
|||
) {
|
||||
while (next !== null) {
|
||||
if (didStatefulHookChange(prev, next)) {
|
||||
return true;
|
||||
} else {
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
indices.push(index);
|
||||
}
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return indices;
|
||||
}
|
||||
|
||||
function getChangedKeys(prev: any, next: any): null | Array<string> {
|
||||
|
|
@ -4354,7 +4315,7 @@ export function attach(
|
|||
if (pseudoKey === undefined) {
|
||||
throw new Error('Expected root pseudo key to be known.');
|
||||
}
|
||||
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
|
||||
const name = pseudoKey.slice(0, pseudoKey.lastIndexOf(':'));
|
||||
const counter = rootDisplayNameCounter.get(name);
|
||||
if (counter === undefined) {
|
||||
throw new Error('Expected counter to be known.');
|
||||
|
|
|
|||
|
|
@ -22,9 +22,15 @@ import type {
|
|||
ElementType,
|
||||
Plugins,
|
||||
} from 'react-devtools-shared/src/types';
|
||||
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
import type {
|
||||
ResolveNativeStyle,
|
||||
SetupNativeStyleEditor,
|
||||
} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
import type {InitBackend} from 'react-devtools-shared/src/backend';
|
||||
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
|
||||
import type {BrowserTheme} from 'react-devtools-shared/src/types';
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type Agent from './agent';
|
||||
|
||||
type BundleType =
|
||||
| 0 // PROD
|
||||
|
|
@ -464,10 +470,18 @@ export type DevToolsProfilingHooks = {
|
|||
markComponentPassiveEffectUnmountStopped: () => void,
|
||||
};
|
||||
|
||||
export type DevToolsBackend = {
|
||||
Agent: Class<Agent>,
|
||||
Bridge: Class<BackendBridge>,
|
||||
initBackend: InitBackend,
|
||||
setupNativeStyleEditor?: SetupNativeStyleEditor,
|
||||
};
|
||||
|
||||
export type DevToolsHook = {
|
||||
listeners: {[key: string]: Array<Handler>, ...},
|
||||
rendererInterfaces: Map<RendererID, RendererInterface>,
|
||||
renderers: Map<RendererID, ReactRenderer>,
|
||||
backends: Map<string, DevToolsBackend>,
|
||||
|
||||
emit: (event: string, data: any) => void,
|
||||
getFiberRoots: (rendererID: RendererID) => Set<Object>,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ import isArray from 'shared/isArray';
|
|||
|
||||
import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
|
||||
|
||||
// TODO: update this to the first React version that has a corresponding DevTools backend
|
||||
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
|
||||
export function hasAssignedBackend(version?: string): boolean {
|
||||
if (version == null || version === '') {
|
||||
return false;
|
||||
}
|
||||
return gte(version, FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER);
|
||||
}
|
||||
|
||||
export function cleanForBridge(
|
||||
data: Object | null,
|
||||
isPathAllowed: (path: Array<string | number>) => boolean,
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@
|
|||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = false;
|
||||
export const enableLogger = true;
|
||||
export const enableNamedHooksFeature = true;
|
||||
export const enableProfilerChangedHookIndices = true;
|
||||
export const enableStyleXFeatures = true;
|
||||
export const isInternalFacebookBuild = true;
|
||||
export const enableProfilerComponentTree = true;
|
||||
|
||||
/************************************************************************
|
||||
* Do not edit the code below.
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@
|
|||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = false;
|
||||
export const enableLogger = false;
|
||||
export const enableNamedHooksFeature = true;
|
||||
export const enableProfilerChangedHookIndices = true;
|
||||
export const enableStyleXFeatures = false;
|
||||
export const isInternalFacebookBuild = false;
|
||||
export const enableProfilerComponentTree = true;
|
||||
|
||||
/************************************************************************
|
||||
* Do not edit the code below.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,5 @@
|
|||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = true;
|
||||
export const enableLogger = false;
|
||||
export const enableNamedHooksFeature = true;
|
||||
export const enableProfilerChangedHookIndices = true;
|
||||
export const enableStyleXFeatures = false;
|
||||
export const isInternalFacebookBuild = false;
|
||||
export const enableProfilerComponentTree = true;
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@
|
|||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = true;
|
||||
export const enableLogger = true;
|
||||
export const enableNamedHooksFeature = true;
|
||||
export const enableProfilerChangedHookIndices = true;
|
||||
export const enableStyleXFeatures = true;
|
||||
export const isInternalFacebookBuild = true;
|
||||
export const enableProfilerComponentTree = true;
|
||||
|
||||
/************************************************************************
|
||||
* Do not edit the code below.
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@
|
|||
|
||||
export const consoleManagedByDevToolsDuringStrictMode = true;
|
||||
export const enableLogger = false;
|
||||
export const enableNamedHooksFeature = true;
|
||||
export const enableProfilerChangedHookIndices = true;
|
||||
export const enableStyleXFeatures = false;
|
||||
export const isInternalFacebookBuild = false;
|
||||
export const enableProfilerComponentTree = true;
|
||||
|
||||
/************************************************************************
|
||||
* Do not edit the code below.
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ export default class Store extends EventEmitter<{
|
|||
// Renderer ID is needed to support inspection fiber props, state, and hooks.
|
||||
_rootIDToRendererID: Map<number, number> = new Map();
|
||||
|
||||
// These options may be initially set by a confiugraiton option when constructing the Store.
|
||||
// These options may be initially set by a configuration option when constructing the Store.
|
||||
_supportsNativeInspection: boolean = true;
|
||||
_supportsProfiling: boolean = false;
|
||||
_supportsReloadAndProfile: boolean = false;
|
||||
|
|
@ -486,7 +486,7 @@ export default class Store extends EventEmitter<{
|
|||
}
|
||||
|
||||
containsElement(id: number): boolean {
|
||||
return this._idToElement.get(id) != null;
|
||||
return this._idToElement.has(id);
|
||||
}
|
||||
|
||||
getElementAtIndex(index: number): Element | null {
|
||||
|
|
@ -539,13 +539,13 @@ export default class Store extends EventEmitter<{
|
|||
}
|
||||
|
||||
getElementIDAtIndex(index: number): number | null {
|
||||
const element: Element | null = this.getElementAtIndex(index);
|
||||
const element = this.getElementAtIndex(index);
|
||||
return element === null ? null : element.id;
|
||||
}
|
||||
|
||||
getElementByID(id: number): Element | null {
|
||||
const element = this._idToElement.get(id);
|
||||
if (element == null) {
|
||||
if (element === undefined) {
|
||||
console.warn(`No element found with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -607,7 +607,10 @@ export default class Store extends EventEmitter<{
|
|||
let currentID = element.parentID;
|
||||
let index = 0;
|
||||
while (true) {
|
||||
const current = ((this._idToElement.get(currentID): any): Element);
|
||||
const current = this._idToElement.get(currentID);
|
||||
if (current === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {children} = current;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
|
|
@ -615,7 +618,12 @@ export default class Store extends EventEmitter<{
|
|||
if (childID === previousID) {
|
||||
break;
|
||||
}
|
||||
const child = ((this._idToElement.get(childID): any): Element);
|
||||
|
||||
const child = this._idToElement.get(childID);
|
||||
if (child === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
index += child.isCollapsed ? 1 : child.weight;
|
||||
}
|
||||
|
||||
|
|
@ -637,7 +645,12 @@ export default class Store extends EventEmitter<{
|
|||
if (rootID === currentID) {
|
||||
break;
|
||||
}
|
||||
const root = ((this._idToElement.get(rootID): any): Element);
|
||||
|
||||
const root = this._idToElement.get(rootID);
|
||||
if (root === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
index += root.weight;
|
||||
}
|
||||
|
||||
|
|
@ -647,7 +660,7 @@ export default class Store extends EventEmitter<{
|
|||
getOwnersListForElement(ownerID: number): Array<Element> {
|
||||
const list: Array<Element> = [];
|
||||
const element = this._idToElement.get(ownerID);
|
||||
if (element != null) {
|
||||
if (element !== undefined) {
|
||||
list.push({
|
||||
...element,
|
||||
depth: 0,
|
||||
|
|
@ -665,8 +678,8 @@ export default class Store extends EventEmitter<{
|
|||
// Seems better to defer the cost, since the set of ids is probably pretty small.
|
||||
const sortedIDs = Array.from(unsortedIDs).sort(
|
||||
(idA, idB) =>
|
||||
((this.getIndexOfElementID(idA): any): number) -
|
||||
((this.getIndexOfElementID(idB): any): number),
|
||||
(this.getIndexOfElementID(idA) || 0) -
|
||||
(this.getIndexOfElementID(idB) || 0),
|
||||
);
|
||||
|
||||
// Next we need to determine the appropriate depth for each element in the list.
|
||||
|
|
@ -677,7 +690,7 @@ export default class Store extends EventEmitter<{
|
|||
// at which point, our depth is just the depth of that node plus one.
|
||||
sortedIDs.forEach(id => {
|
||||
const innerElement = this._idToElement.get(id);
|
||||
if (innerElement != null) {
|
||||
if (innerElement !== undefined) {
|
||||
let parentID = innerElement.parentID;
|
||||
|
||||
let depth = 0;
|
||||
|
|
@ -689,7 +702,7 @@ export default class Store extends EventEmitter<{
|
|||
break;
|
||||
}
|
||||
const parent = this._idToElement.get(parentID);
|
||||
if (parent == null) {
|
||||
if (parent === undefined) {
|
||||
break;
|
||||
}
|
||||
parentID = parent.parentID;
|
||||
|
|
@ -710,7 +723,7 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
getRendererIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
while (current != null) {
|
||||
while (current !== undefined) {
|
||||
if (current.parentID === 0) {
|
||||
const rendererID = this._rootIDToRendererID.get(current.id);
|
||||
return rendererID == null ? null : rendererID;
|
||||
|
|
@ -723,7 +736,7 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
getRootIDForElement(id: number): number | null {
|
||||
let current = this._idToElement.get(id);
|
||||
while (current != null) {
|
||||
while (current !== undefined) {
|
||||
if (current.parentID === 0) {
|
||||
return current.id;
|
||||
} else {
|
||||
|
|
@ -765,10 +778,8 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
const weightDelta = 1 - element.weight;
|
||||
|
||||
let parentElement: void | Element = ((this._idToElement.get(
|
||||
element.parentID,
|
||||
): any): Element);
|
||||
while (parentElement != null) {
|
||||
let parentElement = this._idToElement.get(element.parentID);
|
||||
while (parentElement !== undefined) {
|
||||
// We don't need to break on a collapsed parent in the same way as the expand case below.
|
||||
// That's because collapsing a node doesn't "bubble" and affect its parents.
|
||||
parentElement.weight += weightDelta;
|
||||
|
|
@ -776,7 +787,7 @@ export default class Store extends EventEmitter<{
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let currentElement = element;
|
||||
let currentElement: ?Element = element;
|
||||
while (currentElement != null) {
|
||||
const oldWeight = currentElement.isCollapsed
|
||||
? 1
|
||||
|
|
@ -791,10 +802,8 @@ export default class Store extends EventEmitter<{
|
|||
: currentElement.weight;
|
||||
const weightDelta = newWeight - oldWeight;
|
||||
|
||||
let parentElement: void | Element = ((this._idToElement.get(
|
||||
currentElement.parentID,
|
||||
): any): Element);
|
||||
while (parentElement != null) {
|
||||
let parentElement = this._idToElement.get(currentElement.parentID);
|
||||
while (parentElement !== undefined) {
|
||||
parentElement.weight += weightDelta;
|
||||
if (parentElement.isCollapsed) {
|
||||
// It's important to break on a collapsed parent when expanding nodes.
|
||||
|
|
@ -808,10 +817,8 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
currentElement =
|
||||
currentElement.parentID !== 0
|
||||
? // $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
this.getElementByID(currentElement.parentID)
|
||||
: // $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
null;
|
||||
? this.getElementByID(currentElement.parentID)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -833,7 +840,7 @@ export default class Store extends EventEmitter<{
|
|||
}
|
||||
|
||||
_adjustParentTreeWeight: (
|
||||
parentElement: Element | null,
|
||||
parentElement: ?Element,
|
||||
weightDelta: number,
|
||||
) => void = (parentElement, weightDelta) => {
|
||||
let isInsideCollapsedSubTree = false;
|
||||
|
|
@ -848,9 +855,7 @@ export default class Store extends EventEmitter<{
|
|||
break;
|
||||
}
|
||||
|
||||
parentElement = ((this._idToElement.get(
|
||||
parentElement.parentID,
|
||||
): any): Element);
|
||||
parentElement = this._idToElement.get(parentElement.parentID);
|
||||
}
|
||||
|
||||
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
|
||||
|
|
@ -906,13 +911,16 @@ export default class Store extends EventEmitter<{
|
|||
const stringTable: Array<string | null> = [
|
||||
null, // ID = 0 corresponds to the null string.
|
||||
];
|
||||
const stringTableSize = operations[i++];
|
||||
const stringTableSize = operations[i];
|
||||
i++;
|
||||
|
||||
const stringTableEnd = i + stringTableSize;
|
||||
|
||||
while (i < stringTableEnd) {
|
||||
const nextLength = operations[i++];
|
||||
const nextString = utfDecodeString(
|
||||
(operations.slice(i, i + nextLength): any),
|
||||
);
|
||||
const nextLength = operations[i];
|
||||
i++;
|
||||
|
||||
const nextString = utfDecodeString(operations.slice(i, i + nextLength));
|
||||
stringTable.push(nextString);
|
||||
i += nextLength;
|
||||
}
|
||||
|
|
@ -921,7 +929,7 @@ export default class Store extends EventEmitter<{
|
|||
const operation = operations[i];
|
||||
switch (operation) {
|
||||
case TREE_OPERATION_ADD: {
|
||||
const id = ((operations[i + 1]: any): number);
|
||||
const id = operations[i + 1];
|
||||
const type = ((operations[i + 2]: any): ElementType);
|
||||
|
||||
i += 3;
|
||||
|
|
@ -934,8 +942,6 @@ export default class Store extends EventEmitter<{
|
|||
);
|
||||
}
|
||||
|
||||
let ownerID: number = 0;
|
||||
let parentID: number = ((null: any): number);
|
||||
if (type === ElementTypeRoot) {
|
||||
if (__DEBUG__) {
|
||||
debug('Add', `new root node ${id}`);
|
||||
|
|
@ -997,10 +1003,10 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
haveRootsChanged = true;
|
||||
} else {
|
||||
parentID = ((operations[i]: any): number);
|
||||
const parentID = operations[i];
|
||||
i++;
|
||||
|
||||
ownerID = ((operations[i]: any): number);
|
||||
const ownerID = operations[i];
|
||||
i++;
|
||||
|
||||
const displayNameStringID = operations[i];
|
||||
|
|
@ -1018,17 +1024,17 @@ export default class Store extends EventEmitter<{
|
|||
);
|
||||
}
|
||||
|
||||
if (!this._idToElement.has(parentID)) {
|
||||
const parentElement = this._idToElement.get(parentID);
|
||||
if (parentElement === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentElement = ((this._idToElement.get(
|
||||
parentID,
|
||||
): any): Element);
|
||||
parentElement.children.push(id);
|
||||
|
||||
const [displayNameWithoutHOCs, hocDisplayNames] =
|
||||
|
|
@ -1065,23 +1071,25 @@ export default class Store extends EventEmitter<{
|
|||
break;
|
||||
}
|
||||
case TREE_OPERATION_REMOVE: {
|
||||
const removeLength = ((operations[i + 1]: any): number);
|
||||
const removeLength = operations[i + 1];
|
||||
i += 2;
|
||||
|
||||
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
|
||||
const id = ((operations[i]: any): number);
|
||||
const id = operations[i];
|
||||
const element = this._idToElement.get(id);
|
||||
|
||||
if (!this._idToElement.has(id)) {
|
||||
if (element === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot remove node "${id}" because no matching node was found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
|
||||
const element = ((this._idToElement.get(id): any): Element);
|
||||
const {children, ownerID, parentID, weight} = element;
|
||||
if (children.length > 0) {
|
||||
this._throwAndEmitError(
|
||||
|
|
@ -1091,7 +1099,7 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
this._idToElement.delete(id);
|
||||
|
||||
let parentElement = null;
|
||||
let parentElement: ?Element = null;
|
||||
if (parentID === 0) {
|
||||
if (__DEBUG__) {
|
||||
debug('Remove', `node ${id} root`);
|
||||
|
|
@ -1106,14 +1114,18 @@ export default class Store extends EventEmitter<{
|
|||
if (__DEBUG__) {
|
||||
debug('Remove', `node ${id} from parent ${parentID}`);
|
||||
}
|
||||
parentElement = ((this._idToElement.get(parentID): any): Element);
|
||||
|
||||
parentElement = this._idToElement.get(parentID);
|
||||
if (parentElement === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = parentElement.children.indexOf(id);
|
||||
parentElement.children.splice(index, 1);
|
||||
}
|
||||
|
|
@ -1167,19 +1179,21 @@ export default class Store extends EventEmitter<{
|
|||
break;
|
||||
}
|
||||
case TREE_OPERATION_REORDER_CHILDREN: {
|
||||
const id = ((operations[i + 1]: any): number);
|
||||
const numChildren = ((operations[i + 2]: any): number);
|
||||
const id = operations[i + 1];
|
||||
const numChildren = operations[i + 2];
|
||||
i += 3;
|
||||
|
||||
if (!this._idToElement.has(id)) {
|
||||
const element = this._idToElement.get(id);
|
||||
if (element === undefined) {
|
||||
this._throwAndEmitError(
|
||||
Error(
|
||||
`Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
|
||||
),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const element = ((this._idToElement.get(id): any): Element);
|
||||
const children = element.children;
|
||||
if (children.length !== numChildren) {
|
||||
this._throwAndEmitError(
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function sanitizeForParse(value: any): any | string {
|
|||
value.charAt(0) === "'" &&
|
||||
value.charAt(value.length - 1) === "'"
|
||||
) {
|
||||
return '"' + value.substr(1, value.length - 2) + '"';
|
||||
return '"' + value.slice(1, value.length - 1) + '"';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
|||
strictModeBadge = (
|
||||
<a
|
||||
className={styles.StrictModeNonCompliant}
|
||||
href="https://fb.me/devtools-strict-mode"
|
||||
href="https://react.dev/reference/react/StrictMode"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
title="This component is not running in StrictMode. Click to learn more.">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import {loadModule} from 'react-devtools-shared/src/dynamicImportCache';
|
|||
import FetchFileWithCachingContext from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
|
||||
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import {enableNamedHooksFeature} from 'react-devtools-feature-flags';
|
||||
|
||||
import type {HookNames} from 'react-devtools-shared/src/types';
|
||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
|
|
@ -128,28 +127,26 @@ export function InspectedElementContextController({
|
|||
if (!elementHasChanged && element !== null) {
|
||||
inspectedElement = inspectElement(element, state.path, store, bridge);
|
||||
|
||||
if (enableNamedHooksFeature) {
|
||||
if (typeof hookNamesModuleLoader === 'function') {
|
||||
if (parseHookNames || alreadyLoadedHookNames) {
|
||||
const hookNamesModule = loadModule(hookNamesModuleLoader);
|
||||
if (hookNamesModule !== null) {
|
||||
const {parseHookNames: loadHookNamesFunction, purgeCachedMetadata} =
|
||||
hookNamesModule;
|
||||
if (typeof hookNamesModuleLoader === 'function') {
|
||||
if (parseHookNames || alreadyLoadedHookNames) {
|
||||
const hookNamesModule = loadModule(hookNamesModuleLoader);
|
||||
if (hookNamesModule !== null) {
|
||||
const {parseHookNames: loadHookNamesFunction, purgeCachedMetadata} =
|
||||
hookNamesModule;
|
||||
|
||||
purgeCachedMetadataRef.current = purgeCachedMetadata;
|
||||
purgeCachedMetadataRef.current = purgeCachedMetadata;
|
||||
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.hooks !== null &&
|
||||
loadHookNamesFunction !== null
|
||||
) {
|
||||
hookNames = loadHookNames(
|
||||
element,
|
||||
inspectedElement.hooks,
|
||||
loadHookNamesFunction,
|
||||
fetchFileWithCaching,
|
||||
);
|
||||
}
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.hooks !== null &&
|
||||
loadHookNamesFunction !== null
|
||||
) {
|
||||
hookNames = loadHookNames(
|
||||
element,
|
||||
inspectedElement.hooks,
|
||||
loadHookNamesFunction,
|
||||
fetchFileWithCaching,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ import styles from './InspectedElementHooksTree.css';
|
|||
import useContextMenu from '../../ContextMenu/useContextMenu';
|
||||
import {meta} from '../../../hydration';
|
||||
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
|
||||
import {
|
||||
enableNamedHooksFeature,
|
||||
enableProfilerChangedHookIndices,
|
||||
} from 'react-devtools-feature-flags';
|
||||
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
|
||||
import isArray from 'react-devtools-shared/src/isArray';
|
||||
|
||||
|
|
@ -90,8 +86,7 @@ export function InspectedElementHooksTree({
|
|||
data-testname="InspectedElementHooksTree">
|
||||
<div className={styles.HeaderRow}>
|
||||
<div className={styles.Header}>hooks</div>
|
||||
{enableNamedHooksFeature &&
|
||||
typeof hookNamesModuleLoader === 'function' &&
|
||||
{typeof hookNamesModuleLoader === 'function' &&
|
||||
(!parseHookNames || hookParsingFailed) && (
|
||||
<Toggle
|
||||
className={hookParsingFailed ? styles.ToggleError : null}
|
||||
|
|
@ -225,15 +220,13 @@ function HookView({
|
|||
const isCustomHook = subHooks.length > 0;
|
||||
|
||||
let name = hook.name;
|
||||
if (enableProfilerChangedHookIndices) {
|
||||
if (hookID !== null) {
|
||||
name = (
|
||||
<>
|
||||
<span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
|
||||
{name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (hookID !== null) {
|
||||
name = (
|
||||
<>
|
||||
<span className={styles.PrimitiveHookNumber}>{hookID + 1}</span>
|
||||
{name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const type = typeof value;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ const resource: Resource<
|
|||
(element: Element) => {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
// $FlowFixMe[incompatible-call] found when upgrading Flow
|
||||
return request.promise;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import {SettingsModalContextController} from 'react-devtools-shared/src/devtools
|
|||
import portaledContent from '../portaledContent';
|
||||
import {StoreContext} from '../context';
|
||||
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
|
||||
import {enableProfilerComponentTree} from 'react-devtools-feature-flags';
|
||||
|
||||
import styles from './Profiler.css';
|
||||
|
||||
|
|
@ -56,8 +55,6 @@ function Profiler(_: {}) {
|
|||
const {supportsTimeline} = useContext(StoreContext);
|
||||
|
||||
const isLegacyProfilerSelected = selectedTabID !== 'timeline';
|
||||
const isRightColumnVisible =
|
||||
isLegacyProfilerSelected || enableProfilerComponentTree;
|
||||
|
||||
let view = null;
|
||||
if (didRecordCommits || selectedTabID === 'timeline') {
|
||||
|
|
@ -151,9 +148,7 @@ function Profiler(_: {}) {
|
|||
<ModalDialog />
|
||||
</div>
|
||||
</div>
|
||||
{isRightColumnVisible && (
|
||||
<div className={styles.RightColumn}>{sidebar}</div>
|
||||
)}
|
||||
<div className={styles.RightColumn}>{sidebar}</div>
|
||||
<SettingsModal />
|
||||
</div>
|
||||
</SettingsModalContextController>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
|
||||
import {ProfilerContext} from '../Profiler/ProfilerContext';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
|
|
@ -103,7 +102,7 @@ export default function WhatChanged({fiberID}: Props): React.Node {
|
|||
}
|
||||
|
||||
if (didHooksChange) {
|
||||
if (enableProfilerChangedHookIndices && Array.isArray(hooks)) {
|
||||
if (Array.isArray(hooks)) {
|
||||
changes.push(
|
||||
<div key="hooks" className={styles.Item}>
|
||||
• {hookIndicesToString(hooks)}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ export function createRegExp(string: string): RegExp {
|
|||
// Allow /regex/ syntax with optional last /
|
||||
if (string[0] === '/') {
|
||||
// Cut off first slash
|
||||
string = string.substring(1);
|
||||
string = string.slice(1);
|
||||
// Cut off last slash, but only if it's there
|
||||
if (string[string.length - 1] === '/') {
|
||||
string = string.substring(0, string.length - 1);
|
||||
string = string.slice(0, string.length - 1);
|
||||
}
|
||||
try {
|
||||
return new RegExp(string, 'i');
|
||||
|
|
@ -186,9 +186,9 @@ export function truncateText(text: string, maxLength: number): string {
|
|||
const {length} = text;
|
||||
if (length > maxLength) {
|
||||
return (
|
||||
text.substr(0, Math.floor(maxLength / 2)) +
|
||||
text.slice(0, Math.floor(maxLength / 2)) +
|
||||
'…' +
|
||||
text.substr(length - Math.ceil(maxLength / 2) - 1)
|
||||
text.slice(length - Math.ceil(maxLength / 2) - 1)
|
||||
);
|
||||
} else {
|
||||
return text;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import type {
|
|||
ReactRenderer,
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
DevToolsBackend,
|
||||
} from './backend/types';
|
||||
|
||||
declare var window: any;
|
||||
|
|
@ -506,11 +507,14 @@ export function installHook(target: any): DevToolsHook | null {
|
|||
const rendererInterfaces = new Map<RendererID, RendererInterface>();
|
||||
const listeners: {[string]: Array<Handler>} = {};
|
||||
const renderers = new Map<RendererID, ReactRenderer>();
|
||||
const backends = new Map<string, DevToolsBackend>();
|
||||
|
||||
const hook: DevToolsHook = {
|
||||
rendererInterfaces,
|
||||
listeners,
|
||||
|
||||
backends,
|
||||
|
||||
// Fast Refresh for web relies on this.
|
||||
renderers,
|
||||
|
||||
|
|
|
|||
|
|
@ -693,7 +693,7 @@ function truncateForDisplay(
|
|||
length: number = MAX_PREVIEW_STRING_LENGTH,
|
||||
) {
|
||||
if (string.length > length) {
|
||||
return string.substr(0, length) + '…';
|
||||
return string.slice(0, length) + '…';
|
||||
} else {
|
||||
return string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
* Copyright (c) Meta Platforms, 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
* Copyright (c) Meta Platforms, 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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "react-devtools-timeline",
|
||||
"version": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@elg/speedscope": "1.9.0-a6f84db",
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ const TooltipNetworkMeasure = ({
|
|||
let urlToDisplay = url;
|
||||
if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
|
||||
const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
|
||||
urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
|
||||
urlToDisplay = url.slice(0, half) + '…' + url.slice(url.length - half);
|
||||
}
|
||||
|
||||
const timestampBegin = sendRequestTimestamp;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function trimText(
|
|||
while (startIndex <= stopIndex) {
|
||||
const currentIndex = Math.floor((startIndex + stopIndex) / 2);
|
||||
const trimmedText =
|
||||
currentIndex === maxIndex ? text : text.substr(0, currentIndex) + '…';
|
||||
currentIndex === maxIndex ? text : text.slice(0, currentIndex) + '…';
|
||||
|
||||
if (getTextWidth(context, trimmedText) <= width) {
|
||||
if (longestValidIndex < currentIndex) {
|
||||
|
|
|
|||
|
|
@ -476,10 +476,10 @@ function processTimelineEvent(
|
|||
break;
|
||||
case 'blink.user_timing':
|
||||
if (name.startsWith('--react-version-')) {
|
||||
const [reactVersion] = name.substr(16).split('-');
|
||||
const [reactVersion] = name.slice(16).split('-');
|
||||
currentProfilerData.reactVersion = reactVersion;
|
||||
} else if (name.startsWith('--profiler-version-')) {
|
||||
const [versionString] = name.substr(19).split('-');
|
||||
const [versionString] = name.slice(19).split('-');
|
||||
profilerVersion = parseInt(versionString, 10);
|
||||
if (profilerVersion !== SCHEDULING_PROFILER_VERSION) {
|
||||
throw new InvalidProfileError(
|
||||
|
|
@ -487,7 +487,7 @@ function processTimelineEvent(
|
|||
);
|
||||
}
|
||||
} else if (name.startsWith('--react-lane-labels-')) {
|
||||
const [laneLabelTuplesString] = name.substr(20).split('-');
|
||||
const [laneLabelTuplesString] = name.slice(20).split('-');
|
||||
updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
|
||||
} else if (name.startsWith('--component-')) {
|
||||
processReactComponentMeasure(
|
||||
|
|
@ -497,7 +497,7 @@ function processTimelineEvent(
|
|||
state,
|
||||
);
|
||||
} else if (name.startsWith('--schedule-render-')) {
|
||||
const [laneBitmaskString] = name.substr(18).split('-');
|
||||
const [laneBitmaskString] = name.slice(18).split('-');
|
||||
|
||||
currentProfilerData.schedulingEvents.push({
|
||||
type: 'schedule-render',
|
||||
|
|
@ -506,7 +506,7 @@ function processTimelineEvent(
|
|||
warning: null,
|
||||
});
|
||||
} else if (name.startsWith('--schedule-forced-update-')) {
|
||||
const [laneBitmaskString, componentName] = name.substr(25).split('-');
|
||||
const [laneBitmaskString, componentName] = name.slice(25).split('-');
|
||||
|
||||
const forceUpdateEvent = {
|
||||
type: 'schedule-force-update',
|
||||
|
|
@ -524,7 +524,7 @@ function processTimelineEvent(
|
|||
|
||||
currentProfilerData.schedulingEvents.push(forceUpdateEvent);
|
||||
} else if (name.startsWith('--schedule-state-update-')) {
|
||||
const [laneBitmaskString, componentName] = name.substr(24).split('-');
|
||||
const [laneBitmaskString, componentName] = name.slice(24).split('-');
|
||||
|
||||
const stateUpdateEvent = {
|
||||
type: 'schedule-state-update',
|
||||
|
|
@ -542,7 +542,7 @@ function processTimelineEvent(
|
|||
|
||||
currentProfilerData.schedulingEvents.push(stateUpdateEvent);
|
||||
} else if (name.startsWith('--error-')) {
|
||||
const [componentName, phase, message] = name.substr(8).split('-');
|
||||
const [componentName, phase, message] = name.slice(8).split('-');
|
||||
|
||||
currentProfilerData.thrownErrors.push({
|
||||
componentName,
|
||||
|
|
@ -553,7 +553,7 @@ function processTimelineEvent(
|
|||
});
|
||||
} else if (name.startsWith('--suspense-suspend-')) {
|
||||
const [id, componentName, phase, laneBitmaskString, promiseName] = name
|
||||
.substr(19)
|
||||
.slice(19)
|
||||
.split('-');
|
||||
const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
|
||||
|
||||
|
|
@ -604,7 +604,7 @@ function processTimelineEvent(
|
|||
currentProfilerData.suspenseEvents.push(suspenseEvent);
|
||||
state.unresolvedSuspenseEvents.set(id, suspenseEvent);
|
||||
} else if (name.startsWith('--suspense-resolved-')) {
|
||||
const [id] = name.substr(20).split('-');
|
||||
const [id] = name.slice(20).split('-');
|
||||
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
|
||||
if (suspenseEvent != null) {
|
||||
state.unresolvedSuspenseEvents.delete(id);
|
||||
|
|
@ -613,7 +613,7 @@ function processTimelineEvent(
|
|||
suspenseEvent.resolution = 'resolved';
|
||||
}
|
||||
} else if (name.startsWith('--suspense-rejected-')) {
|
||||
const [id] = name.substr(20).split('-');
|
||||
const [id] = name.slice(20).split('-');
|
||||
const suspenseEvent = state.unresolvedSuspenseEvents.get(id);
|
||||
if (suspenseEvent != null) {
|
||||
state.unresolvedSuspenseEvents.delete(id);
|
||||
|
|
@ -637,7 +637,7 @@ function processTimelineEvent(
|
|||
state.potentialLongNestedUpdate = null;
|
||||
}
|
||||
|
||||
const [laneBitmaskString] = name.substr(15).split('-');
|
||||
const [laneBitmaskString] = name.slice(15).split('-');
|
||||
|
||||
throwIfIncomplete('render', state.measureStack);
|
||||
if (getLastType(state.measureStack) !== 'render-idle') {
|
||||
|
|
@ -682,7 +682,7 @@ function processTimelineEvent(
|
|||
);
|
||||
} else if (name.startsWith('--commit-start-')) {
|
||||
state.nextRenderShouldGenerateNewBatchID = true;
|
||||
const [laneBitmaskString] = name.substr(15).split('-');
|
||||
const [laneBitmaskString] = name.slice(15).split('-');
|
||||
|
||||
markWorkStarted(
|
||||
'commit',
|
||||
|
|
@ -705,7 +705,7 @@ function processTimelineEvent(
|
|||
state.measureStack,
|
||||
);
|
||||
} else if (name.startsWith('--layout-effects-start-')) {
|
||||
const [laneBitmaskString] = name.substr(23).split('-');
|
||||
const [laneBitmaskString] = name.slice(23).split('-');
|
||||
|
||||
markWorkStarted(
|
||||
'layout-effects',
|
||||
|
|
@ -722,7 +722,7 @@ function processTimelineEvent(
|
|||
state.measureStack,
|
||||
);
|
||||
} else if (name.startsWith('--passive-effects-start-')) {
|
||||
const [laneBitmaskString] = name.substr(24).split('-');
|
||||
const [laneBitmaskString] = name.slice(24).split('-');
|
||||
|
||||
markWorkStarted(
|
||||
'passive-effects',
|
||||
|
|
@ -739,7 +739,7 @@ function processTimelineEvent(
|
|||
state.measureStack,
|
||||
);
|
||||
} else if (name.startsWith('--react-internal-module-start-')) {
|
||||
const stackFrameStart = name.substr(30);
|
||||
const stackFrameStart = name.slice(30);
|
||||
|
||||
if (!state.internalModuleStackStringSet.has(stackFrameStart)) {
|
||||
state.internalModuleStackStringSet.add(stackFrameStart);
|
||||
|
|
@ -749,7 +749,7 @@ function processTimelineEvent(
|
|||
state.internalModuleCurrentStackFrame = parsedStackFrameStart;
|
||||
}
|
||||
} else if (name.startsWith('--react-internal-module-stop-')) {
|
||||
const stackFrameStop = name.substr(29);
|
||||
const stackFrameStop = name.slice(29);
|
||||
|
||||
if (!state.internalModuleStackStringSet.has(stackFrameStop)) {
|
||||
state.internalModuleStackStringSet.add(stackFrameStop);
|
||||
|
|
@ -833,7 +833,7 @@ function processReactComponentMeasure(
|
|||
state: ProcessorState,
|
||||
): void {
|
||||
if (name.startsWith('--component-render-start-')) {
|
||||
const [componentName] = name.substr(25).split('-');
|
||||
const [componentName] = name.slice(25).split('-');
|
||||
|
||||
assertNoOverlappingComponentMeasure(state);
|
||||
|
||||
|
|
@ -856,7 +856,7 @@ function processReactComponentMeasure(
|
|||
currentProfilerData.componentMeasures.push(componentMeasure);
|
||||
}
|
||||
} else if (name.startsWith('--component-layout-effect-mount-start-')) {
|
||||
const [componentName] = name.substr(38).split('-');
|
||||
const [componentName] = name.slice(38).split('-');
|
||||
|
||||
assertNoOverlappingComponentMeasure(state);
|
||||
|
||||
|
|
@ -879,7 +879,7 @@ function processReactComponentMeasure(
|
|||
currentProfilerData.componentMeasures.push(componentMeasure);
|
||||
}
|
||||
} else if (name.startsWith('--component-layout-effect-unmount-start-')) {
|
||||
const [componentName] = name.substr(40).split('-');
|
||||
const [componentName] = name.slice(40).split('-');
|
||||
|
||||
assertNoOverlappingComponentMeasure(state);
|
||||
|
||||
|
|
@ -902,7 +902,7 @@ function processReactComponentMeasure(
|
|||
currentProfilerData.componentMeasures.push(componentMeasure);
|
||||
}
|
||||
} else if (name.startsWith('--component-passive-effect-mount-start-')) {
|
||||
const [componentName] = name.substr(39).split('-');
|
||||
const [componentName] = name.slice(39).split('-');
|
||||
|
||||
assertNoOverlappingComponentMeasure(state);
|
||||
|
||||
|
|
@ -925,7 +925,7 @@ function processReactComponentMeasure(
|
|||
currentProfilerData.componentMeasures.push(componentMeasure);
|
||||
}
|
||||
} else if (name.startsWith('--component-passive-effect-unmount-start-')) {
|
||||
const [componentName] = name.substr(41).split('-');
|
||||
const [componentName] = name.slice(41).split('-');
|
||||
|
||||
assertNoOverlappingComponentMeasure(state);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function formatDuration(ms: number): string {
|
|||
|
||||
export function trimString(string: string, length: number): string {
|
||||
if (string.length > length) {
|
||||
return `${string.substr(0, length - 1)}…`;
|
||||
return `${string.slice(0, length - 1)}…`;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,47 @@
|
|||
|
||||
---
|
||||
|
||||
### 4.27.8
|
||||
May 17, 2023
|
||||
|
||||
#### Bugfixes
|
||||
* Fixed duplicated backend activation with multiple renderers ([hoxyq](https://github.com/hoxyq) in [#26807](https://github.com/facebook/react/pull/26807))
|
||||
|
||||
---
|
||||
|
||||
### 4.27.7
|
||||
May 4, 2023
|
||||
|
||||
#### Bugfixes
|
||||
* Fixed to work when browser devtools panel is reopened ([hoxyq](https://github.com/hoxyq) in [#26779](https://github.com/facebook/react/pull/26779))
|
||||
* Fixed to work in Chrome incognito mode ([hoxyq](https://github.com/hoxyq) in [#26765](https://github.com/facebook/react/pull/26765))
|
||||
|
||||
---
|
||||
|
||||
### 4.27.6
|
||||
April 20, 2023
|
||||
|
||||
#### Bugfixes
|
||||
* Fixed backend injection logic for undocked devtools window ([mondaychen](https://github.com/mondaychen) in [#26665](https://github.com/facebook/react/pull/26665))
|
||||
|
||||
#### Other
|
||||
* Use backend manager to support multiple backends in extension ([mondaychen](https://github.com/mondaychen) in [#26615](https://github.com/facebook/react/pull/26615))
|
||||
|
||||
---
|
||||
|
||||
### 4.27.5
|
||||
April 17, 2023
|
||||
|
||||
#### Bugfixes
|
||||
* Fixed copying to clipboard from context menu on Chrome ([hoxyq](https://github.com/hoxyq) in [#26604](https://github.com/facebook/react/pull/26604))
|
||||
* Fixed external link behavior in the standalone app ([Willie-Boy](https://github.com/Willie-Boy) in [#26559](https://github.com/facebook/react/pull/26559))
|
||||
|
||||
#### Other
|
||||
* Provide icon to edge devtools. ([harrygz889](https://github.com/harrygz889) in [#26543](https://github.com/facebook/react/pull/26543))
|
||||
* Forbid editing class instances in props ([hoxyq](https://github.com/hoxyq) in [#26522](https://github.com/facebook/react/pull/26522))
|
||||
* Improved script injection logic in browser extension ([mondaychen](https://github.com/mondaychen) in [#26492](https://github.com/facebook/react/pull/26492))
|
||||
---
|
||||
|
||||
### 4.27.4
|
||||
March 24, 2023
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools",
|
||||
"version": "4.27.4",
|
||||
"version": "4.27.8",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"electron": "^23.1.2",
|
||||
"ip": "^1.1.4",
|
||||
"minimist": "^1.2.3",
|
||||
"react-devtools-core": "4.27.4",
|
||||
"react-devtools-core": "4.27.8",
|
||||
"update-notifier": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,34 +7,5 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
||||
export const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
||||
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||
|
||||
// Assumes there is no parent namespace.
|
||||
export function getIntrinsicNamespace(type: string): string {
|
||||
switch (type) {
|
||||
case 'svg':
|
||||
return SVG_NAMESPACE;
|
||||
case 'math':
|
||||
return MATH_NAMESPACE;
|
||||
default:
|
||||
return HTML_NAMESPACE;
|
||||
}
|
||||
}
|
||||
|
||||
export function getChildNamespace(
|
||||
parentNamespace: string | null,
|
||||
type: string,
|
||||
): string {
|
||||
if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
|
||||
// No (or default) parent namespace: potential entry point.
|
||||
return getIntrinsicNamespace(type);
|
||||
}
|
||||
if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
|
||||
// We're leaving SVG.
|
||||
return HTML_NAMESPACE;
|
||||
}
|
||||
// By default, pass namespace below.
|
||||
return parentNamespace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export function setValueForPropertyOnCustomComponent(
|
|||
) {
|
||||
if (name[0] === 'o' && name[1] === 'n') {
|
||||
const useCapture = name.endsWith('Capture');
|
||||
const eventName = name.substr(2, useCapture ? name.length - 9 : undefined);
|
||||
const eventName = name.slice(2, useCapture ? name.length - 7 : undefined);
|
||||
|
||||
const prevProps = getFiberCurrentPropsFromNode(node);
|
||||
const prevValue = prevProps != null ? prevProps[name] : null;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue