Compare commits
36 Commits
rh/infinit
...
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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -416,7 +416,6 @@ module.exports = {
|
|||
{
|
||||
files: [
|
||||
'packages/react-native-renderer/**/*.js',
|
||||
'packages/react-server-native-relay/**/*.js',
|
||||
],
|
||||
globals: {
|
||||
nativeFabricUIManager: 'readonly',
|
||||
|
|
|
|||
|
|
@ -228,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: |
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import Form from './Form.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');
|
||||
const todos = await res.json();
|
||||
|
|
@ -23,7 +25,7 @@ export default async function App() {
|
|||
</head>
|
||||
<body>
|
||||
<Container>
|
||||
<h1>Hello, world</h1>
|
||||
<h1>{getServerState()}</h1>
|
||||
<Counter />
|
||||
<Counter2 />
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {flushSync} from 'react-dom';
|
||||
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={pending} formAction={action}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Button({action, children}) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<form>
|
||||
<button
|
||||
disabled={isPending}
|
||||
formAction={async () => {
|
||||
// TODO: Migrate to useFormPending once that exists
|
||||
flushSync(() => setIsPending(true));
|
||||
try {
|
||||
const result = await action();
|
||||
console.log(result);
|
||||
} finally {
|
||||
React.startTransition(() => setIsPending(false));
|
||||
}
|
||||
}}>
|
||||
<ButtonDisabledWhilePending action={action}>
|
||||
{children}
|
||||
</button>
|
||||
</ButtonDisabledWhilePending>
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import {flushSync} from 'react-dom';
|
||||
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={async formData => {
|
||||
// TODO: Migrate to useFormPending once that exists
|
||||
flushSync(() => setIsPending(true));
|
||||
try {
|
||||
const result = await action(formData);
|
||||
alert(result);
|
||||
} finally {
|
||||
React.startTransition(() => setIsPending(false));
|
||||
}
|
||||
}}>
|
||||
<form action={action}>
|
||||
<label>
|
||||
Name: <input name="name" />
|
||||
</label>
|
||||
|
|
@ -27,7 +22,7 @@ export default function Form({action, children}) {
|
|||
File: <input type="file" name="file" />
|
||||
</label>
|
||||
<button>Say Hi</button>
|
||||
{isPending ? 'Saving...' : null}
|
||||
<Status />
|
||||
</form>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
let serverState = 'Hello World';
|
||||
|
||||
export function setServerState(message) {
|
||||
serverState = message;
|
||||
}
|
||||
|
||||
export function getServerState() {
|
||||
return serverState;
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
'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}:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
import * as React from 'react';
|
||||
import {use, 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,22 +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,
|
||||
}
|
||||
);
|
||||
|
||||
function Shell({data}) {
|
||||
return use(data);
|
||||
const [root, setRoot] = useState(use(data));
|
||||
updateRoot = setRoot;
|
||||
return root;
|
||||
}
|
||||
|
||||
ReactDOM.hydrateRoot(document, <Shell data={data} />);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
export * from './src/ReactFlightClientStream';
|
||||
export * from './src/ReactFlightClient';
|
||||
|
|
|
|||
|
|
@ -13,28 +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
|
||||
|
|
@ -153,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.
|
||||
|
|
@ -500,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,
|
||||
|
|
@ -624,7 +636,7 @@ export function parseModelString(
|
|||
return value;
|
||||
}
|
||||
|
||||
export function parseModelTuple(
|
||||
function parseModelTuple(
|
||||
response: Response,
|
||||
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
||||
): any {
|
||||
|
|
@ -648,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,
|
||||
|
|
@ -672,7 +692,7 @@ export function resolveModel(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveModule(
|
||||
function resolveModule(
|
||||
response: Response,
|
||||
id: number,
|
||||
model: UninitializedModel,
|
||||
|
|
@ -721,7 +741,7 @@ export function resolveModule(
|
|||
}
|
||||
|
||||
type ErrorWithDigest = Error & {digest?: string};
|
||||
export function resolveErrorProd(
|
||||
function resolveErrorProd(
|
||||
response: Response,
|
||||
id: number,
|
||||
digest: string,
|
||||
|
|
@ -750,7 +770,7 @@ export function resolveErrorProd(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveErrorDev(
|
||||
function resolveErrorDev(
|
||||
response: Response,
|
||||
id: number,
|
||||
digest: string,
|
||||
|
|
@ -781,7 +801,7 @@ export function resolveErrorDev(
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveHint(
|
||||
function resolveHint(
|
||||
response: Response,
|
||||
code: string,
|
||||
model: UninitializedModel,
|
||||
|
|
@ -790,6 +810,105 @@ export function resolveHint(
|
|||
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,146 +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,
|
||||
resolveHint,
|
||||
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.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 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,12 +7,7 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
knownServerReferences,
|
||||
createServerReference,
|
||||
} from './ReactFlightServerReferenceRegistry';
|
||||
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
REACT_ELEMENT_TYPE,
|
||||
|
|
@ -28,6 +23,10 @@ import {
|
|||
} from 'shared/ReactSerializationErrors';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
import type {
|
||||
FulfilledThenable,
|
||||
RejectedThenable,
|
||||
} from '../../shared/ReactTypes';
|
||||
|
||||
type ReactJSONValue =
|
||||
| string
|
||||
|
|
@ -39,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
|
||||
|
|
@ -283,7 +291,6 @@ export function processReply(
|
|||
// Possibly a Date, whose toJSON automatically calls toISOString
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const originalValue = parent[key];
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (originalValue instanceof Date) {
|
||||
return serializeDateFromDateJSON(value);
|
||||
}
|
||||
|
|
@ -363,4 +370,104 @@ export function processReply(
|
|||
}
|
||||
}
|
||||
|
||||
export {createServerReference};
|
||||
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,32 +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';
|
||||
|
||||
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
|
||||
|
||||
type ServerReferenceId = any;
|
||||
|
||||
export const knownServerReferences: WeakMap<
|
||||
Function,
|
||||
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
||||
> = new WeakMap();
|
||||
|
||||
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);
|
||||
};
|
||||
knownServerReferences.set(proxy, {id: id, bound: null});
|
||||
return proxy;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -39,9 +38,6 @@ 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';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "4.27.6",
|
||||
"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.6",
|
||||
"version_name": "4.27.6",
|
||||
"version": "4.27.8",
|
||||
"version_name": "4.27.8",
|
||||
"minimum_chrome_version": "102",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
|
|
|||
|
|
@ -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.6",
|
||||
"version_name": "4.27.6",
|
||||
"version": "4.27.8",
|
||||
"version_name": "4.27.8",
|
||||
"minimum_chrome_version": "102",
|
||||
"icons": {
|
||||
"16": "icons/16-production.png",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||
"version": "4.27.6",
|
||||
"version": "4.27.8",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "@react-devtools",
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,5 +29,6 @@ function setup(hook: ?DevToolsHook) {
|
|||
initBackend,
|
||||
setupNativeStyleEditor,
|
||||
});
|
||||
|
||||
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {COMPACT_VERSION_NAME} from './utils';
|
|||
|
||||
let welcomeHasInitialized = false;
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function welcome(event: $FlowFixMe) {
|
||||
if (
|
||||
event.source !== window ||
|
||||
|
|
@ -59,13 +58,20 @@ function setup(hook: ?DevToolsHook) {
|
|||
|
||||
// register renderers that have already injected themselves.
|
||||
hook.renderers.forEach(renderer => {
|
||||
registerRenderer(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);
|
||||
registerRenderer(renderer, hook);
|
||||
updateRequiredBackends();
|
||||
});
|
||||
|
||||
|
|
@ -78,12 +84,16 @@ function setup(hook: ?DevToolsHook) {
|
|||
|
||||
const requiredBackends = new Set<string>();
|
||||
|
||||
function registerRenderer(renderer: ReactRenderer) {
|
||||
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
|
||||
let version = renderer.reconcilerVersion || renderer.version;
|
||||
if (!hasAssignedBackend(version)) {
|
||||
version = COMPACT_VERSION_NAME;
|
||||
}
|
||||
requiredBackends.add(version);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -91,6 +101,7 @@ function activateBackend(version: string, hook: DevToolsHook) {
|
|||
if (!backend) {
|
||||
throw new Error(`Could not find backend for version "${version}"`);
|
||||
}
|
||||
|
||||
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
|
||||
const bridge = new Bridge({
|
||||
listen(fn) {
|
||||
|
|
@ -151,6 +162,10 @@ function activateBackend(version: string, hook: DevToolsHook) {
|
|||
|
||||
// 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',
|
||||
|
|
|
|||
|
|
@ -6,38 +6,48 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "4.27.6",
|
||||
"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": "
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {hasAssignedBackend} from './utils';
|
|||
|
||||
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
|
||||
|
||||
// this is the backend that is compactible with all older React versions
|
||||
// this is the backend that is compatible with all older React versions
|
||||
function isMatchingRender(version: string): boolean {
|
||||
return !hasAssignedBackend(version);
|
||||
}
|
||||
|
|
@ -31,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',
|
||||
|
|
@ -64,10 +65,6 @@ export function initBackend(
|
|||
];
|
||||
|
||||
const attachRenderer = (id: number, renderer: ReactRenderer) => {
|
||||
// skip if already attached
|
||||
if (renderer.attached) {
|
||||
return;
|
||||
}
|
||||
// only attach if the renderer is compatible with the current version of the backend
|
||||
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
|
||||
return;
|
||||
|
|
@ -102,7 +99,6 @@ export function initBackend(
|
|||
} else {
|
||||
hook.emit('unsupported-renderer-version', id);
|
||||
}
|
||||
renderer.attached = true;
|
||||
};
|
||||
|
||||
// Connect renderers that have already injected themselves.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -171,8 +171,6 @@ export type ReactRenderer = {
|
|||
// 18.0+
|
||||
injectProfilingHooks?: (profilingHooks: DevToolsProfilingHooks) => void,
|
||||
getLaneLabelMap?: () => Map<Lane, string> | null,
|
||||
// set by backend after successful attaching
|
||||
attached?: boolean,
|
||||
...
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.6",
|
||||
"version": "4.27.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@elg/speedscope": "1.9.0-a6f84db",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@
|
|||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-devtools",
|
||||
"version": "4.27.6",
|
||||
"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.6",
|
||||
"react-devtools-core": "4.27.8",
|
||||
"update-notifier": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2791,7 +2791,6 @@ function diffHydratedGenericElement(
|
|||
case 'formAction':
|
||||
if (enableFormActions) {
|
||||
const serverValue = domElement.getAttribute(propKey);
|
||||
const hasFormActionURL = serverValue === EXPECTED_FORM_ACTION_URL;
|
||||
if (typeof value === 'function') {
|
||||
extraAttributes.delete(propKey.toLowerCase());
|
||||
// The server can set these extra properties to implement actions.
|
||||
|
|
@ -2806,13 +2805,14 @@ function diffHydratedGenericElement(
|
|||
extraAttributes.delete('method');
|
||||
extraAttributes.delete('target');
|
||||
}
|
||||
if (hasFormActionURL) {
|
||||
// Expected
|
||||
continue;
|
||||
}
|
||||
warnForPropDifference(propKey, serverValue, value);
|
||||
// Ideally we should be able to warn if the server value was not a function
|
||||
// however since the function can return any of these attributes any way it
|
||||
// wants as a custom progressive enhancement, there's nothing to compare to.
|
||||
// We can check if the function has the $FORM_ACTION property on the client
|
||||
// and if it's not, warn, but that's an unnecessary constraint that they
|
||||
// have to have the extra extension that doesn't do anything on the client.
|
||||
continue;
|
||||
} else if (hasFormActionURL) {
|
||||
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
|
||||
extraAttributes.delete(propKey.toLowerCase());
|
||||
warnForPropDifference(propKey, 'function', value);
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ import {
|
|||
enableHostSingletons,
|
||||
enableTrustedTypesIntegration,
|
||||
diffInCommitPhase,
|
||||
enableFormActions,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
HostComponent,
|
||||
|
|
@ -1038,150 +1039,164 @@ export function isHydratableText(text: string): boolean {
|
|||
return text !== '';
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForInstance(
|
||||
instance: HydratableInstance,
|
||||
type: string,
|
||||
props: Props,
|
||||
): boolean {
|
||||
if (instance.nodeType !== ELEMENT_NODE) {
|
||||
// This is a suspense boundary or Text node.
|
||||
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
|
||||
// and this is a hydration error.
|
||||
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
|
||||
// but it seems reasonable and conservative to reject this as a hydration error as well
|
||||
return false;
|
||||
} else if (
|
||||
instance.nodeName.toLowerCase() !== type.toLowerCase() ||
|
||||
isMarkedHoistable(instance)
|
||||
) {
|
||||
// We are either about to
|
||||
return true;
|
||||
} else {
|
||||
// We have an Element with the right type.
|
||||
const element: Element = (instance: any);
|
||||
const anyProps = (props: any);
|
||||
|
||||
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
|
||||
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
|
||||
// using high entropy attributes for certain types. This technique will fail for strange insertions like
|
||||
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
|
||||
switch (type) {
|
||||
// case 'title':
|
||||
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
|
||||
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
|
||||
case 'meta': {
|
||||
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
|
||||
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
|
||||
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
|
||||
// implications are minimal
|
||||
if (!element.hasAttribute('itemprop')) {
|
||||
// This is a Hoistable
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'link': {
|
||||
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
|
||||
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
|
||||
// matches.
|
||||
const rel = element.getAttribute('rel');
|
||||
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
|
||||
// This is a stylesheet resource
|
||||
return true;
|
||||
} else if (
|
||||
rel !== anyProps.rel ||
|
||||
element.getAttribute('href') !==
|
||||
(anyProps.href == null ? null : anyProps.href) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
|
||||
element.getAttribute('title') !==
|
||||
(anyProps.title == null ? null : anyProps.title)
|
||||
) {
|
||||
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
|
||||
// and title could vary for rel alternate
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'style': {
|
||||
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
|
||||
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
|
||||
if (element.hasAttribute('data-precedence')) {
|
||||
// This is a style resource
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'script': {
|
||||
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
|
||||
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
|
||||
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
|
||||
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
|
||||
// if we learn it is problematic
|
||||
const srcAttr = element.getAttribute('src');
|
||||
if (
|
||||
srcAttr &&
|
||||
element.hasAttribute('async') &&
|
||||
!element.hasAttribute('itemprop')
|
||||
) {
|
||||
// This is an async script resource
|
||||
return true;
|
||||
} else if (
|
||||
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
|
||||
element.getAttribute('type') !==
|
||||
(anyProps.type == null ? null : anyProps.type) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
|
||||
) {
|
||||
// This script is for a different src
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
|
||||
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
|
||||
// that should work in the vast majority of cases.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForTextInstance(
|
||||
instance: HydratableInstance,
|
||||
): boolean {
|
||||
return instance.nodeType === ELEMENT_NODE;
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
): boolean {
|
||||
return instance.nodeType === ELEMENT_NODE;
|
||||
}
|
||||
|
||||
export function canHydrateInstance(
|
||||
instance: HydratableInstance,
|
||||
type: string,
|
||||
props: Props,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | Instance {
|
||||
if (
|
||||
instance.nodeType !== ELEMENT_NODE ||
|
||||
instance.nodeName.toLowerCase() !== type.toLowerCase()
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
return ((instance: any): Instance);
|
||||
while (instance.nodeType === ELEMENT_NODE) {
|
||||
const element: Element = (instance: any);
|
||||
const anyProps = (props: any);
|
||||
if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
|
||||
if (!inRootOrSingleton || !enableHostSingletons) {
|
||||
// Usually we error for mismatched tags.
|
||||
if (
|
||||
enableFormActions &&
|
||||
element.nodeName === 'INPUT' &&
|
||||
(element: any).type === 'hidden'
|
||||
) {
|
||||
// If we have extra hidden inputs, we don't mismatch. This allows us to embed
|
||||
// extra form data in the original form.
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// In root or singleton parents we skip past mismatched instances.
|
||||
} else if (!inRootOrSingleton || !enableHostSingletons) {
|
||||
// Match
|
||||
if (
|
||||
enableFormActions &&
|
||||
type === 'input' &&
|
||||
(element: any).type === 'hidden' &&
|
||||
anyProps.type !== 'hidden'
|
||||
) {
|
||||
// Skip past hidden inputs unless that's what we're looking for. This allows us
|
||||
// embed extra form data in the original form.
|
||||
} else {
|
||||
return element;
|
||||
}
|
||||
} else if (isMarkedHoistable(element)) {
|
||||
// We've already claimed this as a hoistable which isn't hydrated this way so we skip past it.
|
||||
} else {
|
||||
// We have an Element with the right type.
|
||||
|
||||
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
|
||||
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
|
||||
// using high entropy attributes for certain types. This technique will fail for strange insertions like
|
||||
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
|
||||
switch (type) {
|
||||
// case 'title':
|
||||
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
|
||||
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
|
||||
case 'meta': {
|
||||
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
|
||||
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
|
||||
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
|
||||
// implications are minimal
|
||||
if (!element.hasAttribute('itemprop')) {
|
||||
// This is a Hoistable
|
||||
break;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
case 'link': {
|
||||
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
|
||||
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
|
||||
// matches.
|
||||
const rel = element.getAttribute('rel');
|
||||
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
|
||||
// This is a stylesheet resource
|
||||
break;
|
||||
} else if (
|
||||
rel !== anyProps.rel ||
|
||||
element.getAttribute('href') !==
|
||||
(anyProps.href == null ? null : anyProps.href) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
|
||||
element.getAttribute('title') !==
|
||||
(anyProps.title == null ? null : anyProps.title)
|
||||
) {
|
||||
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
|
||||
// and title could vary for rel alternate
|
||||
break;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
case 'style': {
|
||||
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
|
||||
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
|
||||
if (element.hasAttribute('data-precedence')) {
|
||||
// This is a style resource
|
||||
break;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
case 'script': {
|
||||
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
|
||||
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
|
||||
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
|
||||
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
|
||||
// if we learn it is problematic
|
||||
const srcAttr = element.getAttribute('src');
|
||||
if (
|
||||
srcAttr &&
|
||||
element.hasAttribute('async') &&
|
||||
!element.hasAttribute('itemprop')
|
||||
) {
|
||||
// This is an async script resource
|
||||
break;
|
||||
} else if (
|
||||
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
|
||||
element.getAttribute('type') !==
|
||||
(anyProps.type == null ? null : anyProps.type) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
|
||||
) {
|
||||
// This script is for a different src
|
||||
break;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
default: {
|
||||
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
|
||||
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
|
||||
// that should work in the vast majority of cases.
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextInstance = getNextHydratableSibling(element);
|
||||
if (nextInstance === null) {
|
||||
break;
|
||||
}
|
||||
instance = nextInstance;
|
||||
}
|
||||
// This is a suspense boundary or Text node or we got the end.
|
||||
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
|
||||
// and this is a hydration error.
|
||||
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
|
||||
// but it seems reasonable and conservative to reject this as a hydration error as well
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canHydrateTextInstance(
|
||||
instance: HydratableInstance,
|
||||
text: string,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | TextInstance {
|
||||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||||
if (text === '') return null;
|
||||
|
||||
if (instance.nodeType !== TEXT_NODE) {
|
||||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||||
return null;
|
||||
while (instance.nodeType !== TEXT_NODE) {
|
||||
if (!inRootOrSingleton || !enableHostSingletons) {
|
||||
return null;
|
||||
}
|
||||
const nextInstance = getNextHydratableSibling(instance);
|
||||
if (nextInstance === null) {
|
||||
return null;
|
||||
}
|
||||
instance = nextInstance;
|
||||
}
|
||||
// This has now been refined to a text node.
|
||||
return ((instance: any): TextInstance);
|
||||
|
|
@ -1189,9 +1204,17 @@ export function canHydrateTextInstance(
|
|||
|
||||
export function canHydrateSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | SuspenseInstance {
|
||||
if (instance.nodeType !== COMMENT_NODE) {
|
||||
return null;
|
||||
while (instance.nodeType !== COMMENT_NODE) {
|
||||
if (!inRootOrSingleton || !enableHostSingletons) {
|
||||
return null;
|
||||
}
|
||||
const nextInstance = getNextHydratableSibling(instance);
|
||||
if (nextInstance === null) {
|
||||
return null;
|
||||
}
|
||||
instance = nextInstance;
|
||||
}
|
||||
// This has now been refined to a suspense node.
|
||||
return ((instance: any): SuspenseInstance);
|
||||
|
|
@ -1416,12 +1439,14 @@ export function commitHydratedSuspenseInstance(
|
|||
retryIfBlockedOn(suspenseInstance);
|
||||
}
|
||||
|
||||
// @TODO remove this function once float lands and hydrated tail nodes
|
||||
// are controlled by HostSingleton fibers
|
||||
export function shouldDeleteUnhydratedTailInstances(
|
||||
parentType: string,
|
||||
): boolean {
|
||||
return parentType !== 'head' && parentType !== 'body';
|
||||
return (
|
||||
(enableHostSingletons ||
|
||||
(parentType !== 'head' && parentType !== 'body')) &&
|
||||
(!enableFormActions || (parentType !== 'form' && parentType !== 'button'))
|
||||
);
|
||||
}
|
||||
|
||||
export function didNotMatchHydratedContainerTextInstance(
|
||||
|
|
|
|||
|
|
@ -472,7 +472,6 @@ function addTrappedEventListener(
|
|||
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
|
||||
const originalListener = listener;
|
||||
// $FlowFixMe[missing-this-annot]
|
||||
// $FlowFixMe[definition-cycle]
|
||||
listener = function (...p) {
|
||||
removeEventListener(
|
||||
targetContainer,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ if (document.body != null) {
|
|||
}
|
||||
});
|
||||
// documentElement must already exist at this point
|
||||
// $FlowFixMe[incompatible-call]
|
||||
domBodyObserver.observe(document.documentElement, {childList: true});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
checkHtmlStringCoercion,
|
||||
|
|
@ -131,7 +131,7 @@ export type ResponseState = {
|
|||
instructions: InstructionState,
|
||||
|
||||
// state for data streaming format
|
||||
externalRuntimeConfig: BootstrapScriptDescriptor | null,
|
||||
externalRuntimeScript: null | ExternalRuntimeScript,
|
||||
|
||||
// preamble and postamble chunks and state
|
||||
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
|
||||
|
|
@ -161,6 +161,7 @@ const endInlineScript = stringToPrecomputedChunk('</script>');
|
|||
|
||||
const startScriptSrc = stringToPrecomputedChunk('<script src="');
|
||||
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
|
||||
const scriptNonce = stringToPrecomputedChunk('" nonce="');
|
||||
const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
|
||||
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
|
||||
|
||||
|
|
@ -192,6 +193,10 @@ export type BootstrapScriptDescriptor = {
|
|||
src: string,
|
||||
integrity?: string,
|
||||
};
|
||||
export type ExternalRuntimeScript = {
|
||||
src: string,
|
||||
chunks: Array<Chunk | PrecomputedChunk>,
|
||||
};
|
||||
// Allows us to keep track of what we've already written so we can refer back to it.
|
||||
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
|
||||
// is set, the server will send instructions via data attributes (instead of inline scripts)
|
||||
|
|
@ -211,7 +216,7 @@ export function createResponseState(
|
|||
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
|
||||
);
|
||||
const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
|
||||
let externalRuntimeDesc = null;
|
||||
let externalRuntimeScript: null | ExternalRuntimeScript = null;
|
||||
let streamingFormat = ScriptStreamingFormat;
|
||||
if (bootstrapScriptContent !== undefined) {
|
||||
bootstrapChunks.push(
|
||||
|
|
@ -229,12 +234,27 @@ export function createResponseState(
|
|||
if (externalRuntimeConfig !== undefined) {
|
||||
streamingFormat = DataStreamingFormat;
|
||||
if (typeof externalRuntimeConfig === 'string') {
|
||||
externalRuntimeDesc = {
|
||||
externalRuntimeScript = {
|
||||
src: externalRuntimeConfig,
|
||||
integrity: undefined,
|
||||
chunks: [],
|
||||
};
|
||||
pushScriptImpl(externalRuntimeScript.chunks, {
|
||||
src: externalRuntimeConfig,
|
||||
async: true,
|
||||
integrity: undefined,
|
||||
nonce: nonce,
|
||||
});
|
||||
} else {
|
||||
externalRuntimeDesc = externalRuntimeConfig;
|
||||
externalRuntimeScript = {
|
||||
src: externalRuntimeConfig.src,
|
||||
chunks: [],
|
||||
};
|
||||
pushScriptImpl(externalRuntimeScript.chunks, {
|
||||
src: externalRuntimeConfig.src,
|
||||
async: true,
|
||||
integrity: externalRuntimeConfig.integrity,
|
||||
nonce: nonce,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,10 +265,17 @@ export function createResponseState(
|
|||
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
|
||||
const integrity =
|
||||
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
|
||||
|
||||
bootstrapChunks.push(
|
||||
startScriptSrc,
|
||||
stringToChunk(escapeTextForBrowser(src)),
|
||||
);
|
||||
if (nonce) {
|
||||
bootstrapChunks.push(
|
||||
scriptNonce,
|
||||
stringToChunk(escapeTextForBrowser(nonce)),
|
||||
);
|
||||
}
|
||||
if (integrity) {
|
||||
bootstrapChunks.push(
|
||||
scriptIntegirty,
|
||||
|
|
@ -265,10 +292,18 @@ export function createResponseState(
|
|||
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
|
||||
const integrity =
|
||||
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
|
||||
|
||||
bootstrapChunks.push(
|
||||
startModuleSrc,
|
||||
stringToChunk(escapeTextForBrowser(src)),
|
||||
);
|
||||
|
||||
if (nonce) {
|
||||
bootstrapChunks.push(
|
||||
scriptNonce,
|
||||
stringToChunk(escapeTextForBrowser(nonce)),
|
||||
);
|
||||
}
|
||||
if (integrity) {
|
||||
bootstrapChunks.push(
|
||||
scriptIntegirty,
|
||||
|
|
@ -288,7 +323,7 @@ export function createResponseState(
|
|||
streamingFormat,
|
||||
startInlineScript: inlineScriptWithNonce,
|
||||
instructions: NothingSent,
|
||||
externalRuntimeConfig: externalRuntimeDesc,
|
||||
externalRuntimeScript,
|
||||
htmlChunks: null,
|
||||
headChunks: null,
|
||||
hasBody: false,
|
||||
|
|
@ -297,6 +332,7 @@ export function createResponseState(
|
|||
preloadChunks: [],
|
||||
hoistableChunks: [],
|
||||
stylesToHoist: false,
|
||||
nonce,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -632,6 +668,13 @@ function pushStringAttribute(
|
|||
}
|
||||
}
|
||||
|
||||
function makeFormFieldPrefix(responseState: ResponseState): string {
|
||||
// I'm just reusing this counter. It's not really the same namespace as "name".
|
||||
// It could just be its own counter.
|
||||
const id = responseState.nextSuspenseID++;
|
||||
return responseState.idPrefix + id;
|
||||
}
|
||||
|
||||
// Since this will likely be repeated a lot in the HTML, we use a more concise message
|
||||
// than on the client and hopefully it's googleable.
|
||||
const actionJavaScriptURL = stringToPrecomputedChunk(
|
||||
|
|
@ -641,6 +684,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
|
|||
),
|
||||
);
|
||||
|
||||
const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
|
||||
|
||||
function pushAdditionalFormField(
|
||||
this: Array<Chunk | PrecomputedChunk>,
|
||||
value: string | File,
|
||||
key: string,
|
||||
): void {
|
||||
const target: Array<Chunk | PrecomputedChunk> = this;
|
||||
target.push(startHiddenInputChunk);
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(
|
||||
'File/Blob fields are not yet supported in progressive forms. ' +
|
||||
'It probably means you are closing over binary data or FormData in a Server Action.',
|
||||
);
|
||||
}
|
||||
pushStringAttribute(target, 'name', key);
|
||||
pushStringAttribute(target, 'value', value);
|
||||
target.push(endOfStartTagSelfClosing);
|
||||
}
|
||||
|
||||
function pushAdditionalFormFields(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
formData: null | FormData,
|
||||
) {
|
||||
if (formData !== null) {
|
||||
// $FlowFixMe[prop-missing]: FormData has forEach.
|
||||
formData.forEach(pushAdditionalFormField, target);
|
||||
}
|
||||
}
|
||||
|
||||
function pushFormActionAttribute(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
responseState: ResponseState,
|
||||
|
|
@ -649,7 +722,8 @@ function pushFormActionAttribute(
|
|||
formMethod: any,
|
||||
formTarget: any,
|
||||
name: any,
|
||||
): void {
|
||||
): null | FormData {
|
||||
let formData = null;
|
||||
if (enableFormActions && typeof formAction === 'function') {
|
||||
// Function form actions cannot control the form properties
|
||||
if (__DEV__) {
|
||||
|
|
@ -678,37 +752,55 @@ function pushFormActionAttribute(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
|
||||
// manually submitted or if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
target.push(
|
||||
attributeSeparator,
|
||||
stringToChunk('formAction'),
|
||||
attributeAssign,
|
||||
actionJavaScriptURL,
|
||||
attributeEnd,
|
||||
);
|
||||
injectFormReplayingRuntime(responseState);
|
||||
} else {
|
||||
// Plain form actions support all the properties, so we have to emit them.
|
||||
if (name !== null) {
|
||||
pushAttribute(target, 'name', name);
|
||||
}
|
||||
if (formAction !== null) {
|
||||
pushAttribute(target, 'formAction', formAction);
|
||||
}
|
||||
if (formEncType !== null) {
|
||||
pushAttribute(target, 'formEncType', formEncType);
|
||||
}
|
||||
if (formMethod !== null) {
|
||||
pushAttribute(target, 'formMethod', formMethod);
|
||||
}
|
||||
if (formTarget !== null) {
|
||||
pushAttribute(target, 'formTarget', formTarget);
|
||||
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
|
||||
if (typeof customAction === 'function') {
|
||||
// This action has a custom progressive enhancement form that can submit the form
|
||||
// back to the server if it's invoked before hydration. Such as a Server Action.
|
||||
const prefix = makeFormFieldPrefix(responseState);
|
||||
const customFields = formAction.$$FORM_ACTION(prefix);
|
||||
name = customFields.name;
|
||||
formAction = customFields.action || '';
|
||||
formEncType = customFields.encType;
|
||||
formMethod = customFields.method;
|
||||
formTarget = customFields.target;
|
||||
formData = customFields.data;
|
||||
} else {
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
|
||||
// manually submitted or if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
target.push(
|
||||
attributeSeparator,
|
||||
stringToChunk('formAction'),
|
||||
attributeAssign,
|
||||
actionJavaScriptURL,
|
||||
attributeEnd,
|
||||
);
|
||||
name = null;
|
||||
formAction = null;
|
||||
formEncType = null;
|
||||
formMethod = null;
|
||||
formTarget = null;
|
||||
injectFormReplayingRuntime(responseState);
|
||||
}
|
||||
}
|
||||
if (name != null) {
|
||||
pushAttribute(target, 'name', name);
|
||||
}
|
||||
if (formAction != null) {
|
||||
pushAttribute(target, 'formAction', formAction);
|
||||
}
|
||||
if (formEncType != null) {
|
||||
pushAttribute(target, 'formEncType', formEncType);
|
||||
}
|
||||
if (formMethod != null) {
|
||||
pushAttribute(target, 'formMethod', formMethod);
|
||||
}
|
||||
if (formTarget != null) {
|
||||
pushAttribute(target, 'formTarget', formTarget);
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
function pushAttribute(
|
||||
|
|
@ -1273,7 +1365,7 @@ function injectFormReplayingRuntime(responseState: ResponseState): void {
|
|||
// to emit anything. It's always used.
|
||||
if (
|
||||
(responseState.instructions & SentFormReplayingRuntime) === NothingSent &&
|
||||
(!enableFizzExternalRuntime || !responseState.externalRuntimeConfig)
|
||||
(!enableFizzExternalRuntime || !responseState.externalRuntimeScript)
|
||||
) {
|
||||
responseState.instructions |= SentFormReplayingRuntime;
|
||||
responseState.bootstrapChunks.unshift(
|
||||
|
|
@ -1330,6 +1422,8 @@ function pushStartForm(
|
|||
}
|
||||
}
|
||||
|
||||
let formData = null;
|
||||
let formActionName = null;
|
||||
if (enableFormActions && typeof formAction === 'function') {
|
||||
// Function form actions cannot control the form properties
|
||||
if (__DEV__) {
|
||||
|
|
@ -1352,36 +1446,60 @@ function pushStartForm(
|
|||
);
|
||||
}
|
||||
}
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
|
||||
// manually submitted or if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
target.push(
|
||||
attributeSeparator,
|
||||
stringToChunk('action'),
|
||||
attributeAssign,
|
||||
actionJavaScriptURL,
|
||||
attributeEnd,
|
||||
);
|
||||
injectFormReplayingRuntime(responseState);
|
||||
} else {
|
||||
// Plain form actions support all the properties, so we have to emit them.
|
||||
if (formAction !== null) {
|
||||
pushAttribute(target, 'action', formAction);
|
||||
}
|
||||
if (formEncType !== null) {
|
||||
pushAttribute(target, 'encType', formEncType);
|
||||
}
|
||||
if (formMethod !== null) {
|
||||
pushAttribute(target, 'method', formMethod);
|
||||
}
|
||||
if (formTarget !== null) {
|
||||
pushAttribute(target, 'target', formTarget);
|
||||
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
|
||||
if (typeof customAction === 'function') {
|
||||
// This action has a custom progressive enhancement form that can submit the form
|
||||
// back to the server if it's invoked before hydration. Such as a Server Action.
|
||||
const prefix = makeFormFieldPrefix(responseState);
|
||||
const customFields = formAction.$$FORM_ACTION(prefix);
|
||||
formAction = customFields.action || '';
|
||||
formEncType = customFields.encType;
|
||||
formMethod = customFields.method;
|
||||
formTarget = customFields.target;
|
||||
formData = customFields.data;
|
||||
formActionName = customFields.name;
|
||||
} else {
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
|
||||
// manually submitted or if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
target.push(
|
||||
attributeSeparator,
|
||||
stringToChunk('action'),
|
||||
attributeAssign,
|
||||
actionJavaScriptURL,
|
||||
attributeEnd,
|
||||
);
|
||||
formAction = null;
|
||||
formEncType = null;
|
||||
formMethod = null;
|
||||
formTarget = null;
|
||||
injectFormReplayingRuntime(responseState);
|
||||
}
|
||||
}
|
||||
if (formAction != null) {
|
||||
pushAttribute(target, 'action', formAction);
|
||||
}
|
||||
if (formEncType != null) {
|
||||
pushAttribute(target, 'encType', formEncType);
|
||||
}
|
||||
if (formMethod != null) {
|
||||
pushAttribute(target, 'method', formMethod);
|
||||
}
|
||||
if (formTarget != null) {
|
||||
pushAttribute(target, 'target', formTarget);
|
||||
}
|
||||
|
||||
target.push(endOfStartTag);
|
||||
|
||||
if (formActionName !== null) {
|
||||
target.push(startHiddenInputChunk);
|
||||
pushStringAttribute(target, 'name', formActionName);
|
||||
target.push(endOfStartTagSelfClosing);
|
||||
pushAdditionalFormFields(target, formData);
|
||||
}
|
||||
|
||||
pushInnerHTML(target, innerHTML, children);
|
||||
if (typeof children === 'string') {
|
||||
// Special case children as a string to avoid the unnecessary comment.
|
||||
|
|
@ -1474,7 +1592,7 @@ function pushInput(
|
|||
}
|
||||
}
|
||||
|
||||
pushFormActionAttribute(
|
||||
const formData = pushFormActionAttribute(
|
||||
target,
|
||||
responseState,
|
||||
formAction,
|
||||
|
|
@ -1525,6 +1643,10 @@ function pushInput(
|
|||
}
|
||||
|
||||
target.push(endOfStartTagSelfClosing);
|
||||
|
||||
// We place any additional hidden form fields after the input.
|
||||
pushAdditionalFormFields(target, formData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1592,7 +1714,7 @@ function pushStartButton(
|
|||
}
|
||||
}
|
||||
|
||||
pushFormActionAttribute(
|
||||
const formData = pushFormActionAttribute(
|
||||
target,
|
||||
responseState,
|
||||
formAction,
|
||||
|
|
@ -1603,6 +1725,10 @@ function pushStartButton(
|
|||
);
|
||||
|
||||
target.push(endOfStartTag);
|
||||
|
||||
// We place any additional hidden form fields we need to include inside the button itself.
|
||||
pushAdditionalFormFields(target, formData);
|
||||
|
||||
pushInnerHTML(target, innerHTML, children);
|
||||
if (typeof children === 'string') {
|
||||
// Special case children as a string to avoid the unnecessary comment.
|
||||
|
|
@ -1610,6 +1736,7 @@ function pushStartButton(
|
|||
target.push(stringToChunk(encodeHTMLTextNode(children)));
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
|
|
@ -4058,15 +4185,15 @@ export function writePreamble(
|
|||
if (
|
||||
enableFizzExternalRuntime &&
|
||||
!willFlushAllSegments &&
|
||||
responseState.externalRuntimeConfig
|
||||
responseState.externalRuntimeScript
|
||||
) {
|
||||
// If the root segment is incomplete due to suspended tasks
|
||||
// (e.g. willFlushAllSegments = false) and we are using data
|
||||
// streaming format, ensure the external runtime is sent.
|
||||
// (User code could choose to send this even earlier by calling
|
||||
// preinit(...), if they know they will suspend).
|
||||
const {src, integrity} = responseState.externalRuntimeConfig;
|
||||
internalPreinitScript(resources, src, integrity);
|
||||
const {src, chunks} = responseState.externalRuntimeScript;
|
||||
internalPreinitScript(resources, src, chunks);
|
||||
}
|
||||
|
||||
const htmlChunks = responseState.htmlChunks;
|
||||
|
|
@ -5342,30 +5469,22 @@ function preinit(href: string, options: PreinitOptions): void {
|
|||
}
|
||||
}
|
||||
|
||||
// This method is trusted. It must only be called from within this codebase and it assumes the arguments
|
||||
// conform to the types because no user input is being passed in. It also assumes that it is being called as
|
||||
// part of a work or flush loop and therefore does not need to request Fizz to flush Resources.
|
||||
function internalPreinitScript(
|
||||
resources: Resources,
|
||||
src: string,
|
||||
integrity: ?string,
|
||||
chunks: Array<Chunk | PrecomputedChunk>,
|
||||
): void {
|
||||
const key = getResourceKey('script', src);
|
||||
let resource = resources.scriptsMap.get(key);
|
||||
if (!resource) {
|
||||
resource = {
|
||||
type: 'script',
|
||||
chunks: [],
|
||||
chunks,
|
||||
state: NoState,
|
||||
props: null,
|
||||
};
|
||||
resources.scriptsMap.set(key, resource);
|
||||
resources.scripts.add(resource);
|
||||
pushScriptImpl(resource.chunks, {
|
||||
async: true,
|
||||
src,
|
||||
integrity,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import type {
|
||||
BootstrapScriptDescriptor,
|
||||
ExternalRuntimeScript,
|
||||
FormatContext,
|
||||
StreamingFormat,
|
||||
InstructionState,
|
||||
|
|
@ -48,7 +49,7 @@ export type ResponseState = {
|
|||
streamingFormat: StreamingFormat,
|
||||
startInlineScript: PrecomputedChunk,
|
||||
instructions: InstructionState,
|
||||
externalRuntimeConfig: BootstrapScriptDescriptor | null,
|
||||
externalRuntimeScript: null | ExternalRuntimeScript,
|
||||
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
|
||||
headChunks: null | Array<Chunk | PrecomputedChunk>,
|
||||
hasBody: boolean,
|
||||
|
|
@ -85,7 +86,7 @@ export function createResponseState(
|
|||
streamingFormat: responseState.streamingFormat,
|
||||
startInlineScript: responseState.startInlineScript,
|
||||
instructions: responseState.instructions,
|
||||
externalRuntimeConfig: responseState.externalRuntimeConfig,
|
||||
externalRuntimeScript: responseState.externalRuntimeScript,
|
||||
htmlChunks: responseState.htmlChunks,
|
||||
headChunks: responseState.headChunks,
|
||||
hasBody: responseState.hasBody,
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export {
|
|||
preconnect,
|
||||
preload,
|
||||
preinit,
|
||||
experimental_useFormStatus,
|
||||
} from './src/server/ReactDOMServerRenderingStub';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ let React;
|
|||
let ReactDOMServer;
|
||||
let ReactDOMClient;
|
||||
let useFormStatus;
|
||||
let useOptimistic;
|
||||
|
||||
describe('ReactDOMFizzForm', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -30,6 +31,7 @@ describe('ReactDOMFizzForm', () => {
|
|||
ReactDOMServer = require('react-dom/server.browser');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
useFormStatus = require('react-dom').experimental_useFormStatus;
|
||||
useOptimistic = require('react').experimental_useOptimistic;
|
||||
act = require('internal-test-utils').act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
|
@ -181,7 +183,7 @@ describe('ReactDOMFizzForm', () => {
|
|||
});
|
||||
|
||||
// @gate enableFormActions || !__DEV__
|
||||
it('should warn when passing a string during SSR and function during hydration', async () => {
|
||||
it('should ideally warn when passing a string during SSR and function during hydration', async () => {
|
||||
function action(formData) {}
|
||||
function App({isClient}) {
|
||||
return (
|
||||
|
|
@ -193,13 +195,10 @@ describe('ReactDOMFizzForm', () => {
|
|||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
'Prop `action` did not match. Server: "action" Client: "function action(formData) {}"',
|
||||
);
|
||||
// This should ideally warn because only the client provides a function that doesn't line up.
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
|
||||
});
|
||||
});
|
||||
|
||||
// @gate enableFormActions || !__DEV__
|
||||
|
|
@ -453,4 +452,150 @@ describe('ReactDOMFizzForm', () => {
|
|||
expect(deletedTitle).toBe('Hello');
|
||||
expect(rootActionCalled).toBe(false);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
it('useOptimistic returns passthrough value', async () => {
|
||||
function App() {
|
||||
const [optimisticState] = useOptimistic('hi');
|
||||
return optimisticState;
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
expect(container.textContent).toBe('hi');
|
||||
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
expect(container.textContent).toBe('hi');
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('can provide a custom action on the server for actions', async () => {
|
||||
const ref = React.createRef();
|
||||
let foo;
|
||||
|
||||
function action(formData) {
|
||||
foo = formData.get('foo');
|
||||
}
|
||||
action.$$FORM_ACTION = function (identifierPrefix) {
|
||||
const extraFields = new FormData();
|
||||
extraFields.append(identifierPrefix + 'hello', 'world');
|
||||
return {
|
||||
action: this.name,
|
||||
name: identifierPrefix,
|
||||
method: 'POST',
|
||||
encType: 'multipart/form-data',
|
||||
target: 'self',
|
||||
data: extraFields,
|
||||
};
|
||||
};
|
||||
function App() {
|
||||
return (
|
||||
<form action={action} ref={ref} method={null}>
|
||||
<input type="text" name="foo" defaultValue="bar" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
const form = container.firstChild;
|
||||
expect(form.getAttribute('action')).toBe('action');
|
||||
expect(form.getAttribute('method')).toBe('POST');
|
||||
expect(form.getAttribute('enctype')).toBe('multipart/form-data');
|
||||
expect(form.getAttribute('target')).toBe('self');
|
||||
const formActionName = form.firstChild.getAttribute('name');
|
||||
expect(
|
||||
container
|
||||
.querySelector('input[name="' + formActionName + 'hello"]')
|
||||
.getAttribute('value'),
|
||||
).toBe('world');
|
||||
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
|
||||
submit(ref.current);
|
||||
|
||||
expect(foo).toBe('bar');
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('can provide a custom action on buttons the server for actions', async () => {
|
||||
const inputRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
let foo;
|
||||
|
||||
function action(formData) {
|
||||
foo = formData.get('foo');
|
||||
}
|
||||
action.$$FORM_ACTION = function (identifierPrefix) {
|
||||
const extraFields = new FormData();
|
||||
extraFields.append(identifierPrefix + 'hello', 'world');
|
||||
return {
|
||||
action: this.name,
|
||||
name: identifierPrefix,
|
||||
method: 'POST',
|
||||
encType: 'multipart/form-data',
|
||||
target: 'self',
|
||||
data: extraFields,
|
||||
};
|
||||
};
|
||||
function App() {
|
||||
return (
|
||||
<form>
|
||||
<input type="hidden" name="foo" value="bar" />
|
||||
<input
|
||||
type="submit"
|
||||
formAction={action}
|
||||
method={null}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<button formAction={action} ref={buttonRef} target={null} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
|
||||
const input = container.getElementsByTagName('input')[1];
|
||||
const button = container.getElementsByTagName('button')[0];
|
||||
expect(input.getAttribute('formaction')).toBe('action');
|
||||
expect(input.getAttribute('formmethod')).toBe('POST');
|
||||
expect(input.getAttribute('formenctype')).toBe('multipart/form-data');
|
||||
expect(input.getAttribute('formtarget')).toBe('self');
|
||||
expect(button.getAttribute('formaction')).toBe('action');
|
||||
expect(button.getAttribute('formmethod')).toBe('POST');
|
||||
expect(button.getAttribute('formenctype')).toBe('multipart/form-data');
|
||||
expect(button.getAttribute('formtarget')).toBe('self');
|
||||
const inputName = input.getAttribute('name');
|
||||
const buttonName = button.getAttribute('name');
|
||||
expect(
|
||||
container
|
||||
.querySelector('input[name="' + inputName + 'hello"]')
|
||||
.getAttribute('value'),
|
||||
).toBe('world');
|
||||
expect(
|
||||
container
|
||||
.querySelector('input[name="' + buttonName + 'hello"]')
|
||||
.getAttribute('value'),
|
||||
).toBe('world');
|
||||
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
|
||||
submit(inputRef.current);
|
||||
|
||||
expect(foo).toBe('bar');
|
||||
|
||||
foo = null;
|
||||
|
||||
submit(buttonRef.current);
|
||||
|
||||
expect(foo).toBe('bar');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ let JSDOM;
|
|||
let Stream;
|
||||
let Scheduler;
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let ReactDOMFizzServer;
|
||||
let Suspense;
|
||||
|
|
@ -73,6 +74,7 @@ describe('ReactDOMFizzServer', () => {
|
|||
|
||||
Scheduler = require('scheduler');
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
ReactDOMFizzServer = require('react-dom/server');
|
||||
Stream = require('stream');
|
||||
|
|
@ -574,7 +576,7 @@ describe('ReactDOMFizzServer', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should support nonce scripts', async () => {
|
||||
it('should support nonce for bootstrap and runtime scripts', async () => {
|
||||
CSPnonce = 'R4nd0m';
|
||||
try {
|
||||
let resolve;
|
||||
|
|
@ -591,11 +593,26 @@ describe('ReactDOMFizzServer', () => {
|
|||
<Lazy text="Hello" />
|
||||
</Suspense>
|
||||
</div>,
|
||||
{nonce: 'R4nd0m'},
|
||||
{
|
||||
nonce: 'R4nd0m',
|
||||
bootstrapScriptContent: 'function noop(){}',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
|
||||
|
||||
// check that there are 4 scripts with a matching nonce:
|
||||
// The runtime script, an inline bootstrap script, and two src scripts
|
||||
expect(
|
||||
Array.from(container.getElementsByTagName('script')).filter(
|
||||
node => node.getAttribute('nonce') === CSPnonce,
|
||||
).length,
|
||||
).toEqual(4);
|
||||
|
||||
await act(() => {
|
||||
resolve({default: Text});
|
||||
});
|
||||
|
|
@ -605,6 +622,53 @@ describe('ReactDOMFizzServer', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should not automatically add nonce to rendered scripts', async () => {
|
||||
CSPnonce = 'R4nd0m';
|
||||
try {
|
||||
await act(async () => {
|
||||
const {pipe} = renderToPipeableStream(
|
||||
<html>
|
||||
<body>
|
||||
<script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>
|
||||
<script nonce={CSPnonce} src="foo" async={true} />
|
||||
<script src="bar" />
|
||||
<script src="baz" integrity="qux" async={true} />
|
||||
<script type="module" src="quux" async={true} />
|
||||
<script type="module" src="corge" async={true} />
|
||||
<script
|
||||
type="module"
|
||||
src="grault"
|
||||
integrity="garply"
|
||||
async={true}
|
||||
/>
|
||||
</body>
|
||||
</html>,
|
||||
{
|
||||
nonce: CSPnonce,
|
||||
},
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(
|
||||
stripExternalRuntimeInNodes(
|
||||
document.getElementsByTagName('script'),
|
||||
renderOptions.unstable_externalRuntimeSrc,
|
||||
).map(n => n.outerHTML),
|
||||
).toEqual([
|
||||
`<script nonce="${CSPnonce}" src="foo" async=""></script>`,
|
||||
`<script src="baz" integrity="qux" async=""></script>`,
|
||||
`<script type="module" src="quux" async=""></script>`,
|
||||
`<script type="module" src="corge" async=""></script>`,
|
||||
`<script type="module" src="grault" integrity="garply" async=""></script>`,
|
||||
`<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,
|
||||
`<script src="bar"></script>`,
|
||||
]);
|
||||
} finally {
|
||||
CSPnonce = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should client render a boundary if a lazy component rejects', async () => {
|
||||
let rejectComponent;
|
||||
const LazyComponent = React.lazy(() => {
|
||||
|
|
@ -2445,6 +2509,98 @@ describe('ReactDOMFizzServer', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('can hydrate uSES in StrictMode with different client and server snapshot (sync)', async () => {
|
||||
function subscribe() {
|
||||
return () => {};
|
||||
}
|
||||
function getClientSnapshot() {
|
||||
return 'Yay!';
|
||||
}
|
||||
function getServerSnapshot() {
|
||||
return 'Nay!';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const value = useSyncExternalStore(
|
||||
subscribe,
|
||||
getClientSnapshot,
|
||||
getServerSnapshot,
|
||||
);
|
||||
Scheduler.log(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const element = (
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const {pipe} = renderToPipeableStream(element);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
assertLog(['Nay!']);
|
||||
expect(getVisibleChildren(container)).toEqual('Nay!');
|
||||
|
||||
await clientAct(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(container, element);
|
||||
});
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual('Yay!');
|
||||
assertLog(['Nay!', 'Yay!']);
|
||||
});
|
||||
|
||||
it('can hydrate uSES in StrictMode with different client and server snapshot (concurrent)', async () => {
|
||||
function subscribe() {
|
||||
return () => {};
|
||||
}
|
||||
function getClientSnapshot() {
|
||||
return 'Yay!';
|
||||
}
|
||||
function getServerSnapshot() {
|
||||
return 'Nay!';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const value = useSyncExternalStore(
|
||||
subscribe,
|
||||
getClientSnapshot,
|
||||
getServerSnapshot,
|
||||
);
|
||||
Scheduler.log(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const element = (
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const {pipe} = renderToPipeableStream(element);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
assertLog(['Nay!']);
|
||||
expect(getVisibleChildren(container)).toEqual('Nay!');
|
||||
|
||||
await clientAct(() => {
|
||||
React.startTransition(() => {
|
||||
ReactDOMClient.hydrateRoot(container, element);
|
||||
});
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(container)).toEqual('Yay!');
|
||||
assertLog(['Nay!', 'Yay!']);
|
||||
});
|
||||
|
||||
it(
|
||||
'errors during hydration force a client render at the nearest Suspense ' +
|
||||
'boundary, and during the client render it recovers',
|
||||
|
|
@ -3705,7 +3861,7 @@ describe('ReactDOMFizzServer', () => {
|
|||
Array.from(document.head.getElementsByTagName('script')).map(
|
||||
n => n.outerHTML,
|
||||
),
|
||||
).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
|
||||
).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
|
||||
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
|
|
|
|||
|
|
@ -486,4 +486,21 @@ describe('ReactDOMFizzServerBrowser', () => {
|
|||
'<!DOCTYPE html><html><head><title>foo</title></head><body>bar</body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should support nonce attribute for bootstrap scripts', async () => {
|
||||
const nonce = 'R4nd0m';
|
||||
const stream = await ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>hello world</div>,
|
||||
{
|
||||
nonce,
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
`"<div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -695,4 +695,41 @@ describe('ReactDOMServerHydration', () => {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
it('allows rendering extra hidden inputs in a form', async () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML =
|
||||
'<form>' +
|
||||
'<input type="hidden" /><input type="hidden" name="a" value="A" />' +
|
||||
'<input type="hidden" /><input type="submit" name="b" value="B" />' +
|
||||
'<input type="hidden" /><button name="c" value="C"></button>' +
|
||||
'<input type="hidden" />' +
|
||||
'</form>';
|
||||
const form = element.firstChild;
|
||||
const ref = React.createRef();
|
||||
const a = React.createRef();
|
||||
const b = React.createRef();
|
||||
const c = React.createRef();
|
||||
await act(async () => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
element,
|
||||
<form ref={ref}>
|
||||
<input type="hidden" name="a" value="A" ref={a} />
|
||||
<input type="submit" name="b" value="B" ref={b} />
|
||||
<button name="c" value="C" ref={c} />
|
||||
</form>,
|
||||
);
|
||||
});
|
||||
|
||||
// The content should not have been client rendered.
|
||||
expect(ref.current).toBe(form);
|
||||
|
||||
expect(a.current.name).toBe('a');
|
||||
expect(a.current.value).toBe('A');
|
||||
expect(b.current.name).toBe('b');
|
||||
expect(b.current.value).toBe('B');
|
||||
expect(c.current.name).toBe('c');
|
||||
expect(c.current.value).toBe('C');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -81,4 +81,16 @@ describe('react-dom-server-rendering-stub', () => {
|
|||
);
|
||||
expect(x).toBe(false);
|
||||
});
|
||||
|
||||
// @gate enableFormActions
|
||||
// @gate enableAsyncActions
|
||||
it('exports experimental_useFormStatus', async () => {
|
||||
function App() {
|
||||
const {pending} = ReactDOM.experimental_useFormStatus();
|
||||
return 'Pending: ' + pending;
|
||||
}
|
||||
|
||||
const result = await ReactDOMFizzServer.renderToStaticMarkup(<App />);
|
||||
expect(result).toEqual('Pending: false');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
export {preinit, preload, preconnect, prefetchDNS} from '../ReactDOMFloat';
|
||||
export {useFormStatus as experimental_useFormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
|
||||
|
||||
export function createPortal() {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ async function executeScript(script: Element) {
|
|||
} else {
|
||||
const newScript = ownerDocument.createElement('script');
|
||||
newScript.textContent = script.textContent;
|
||||
// make sure to add nonce back to script if it exists
|
||||
const scriptNonce = script.getAttribute('nonce');
|
||||
if (scriptNonce) {
|
||||
newScript.setAttribute('nonce', scriptNonce);
|
||||
}
|
||||
|
||||
parent.insertBefore(newScript, script);
|
||||
parent.removeChild(script);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
"directory": "packages/react-native-renderer"
|
||||
},
|
||||
"dependencies": {
|
||||
"scheduler": "^0.11.0"
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {dispatchEvent} from './ReactFabricEventEmitter';
|
|||
import {
|
||||
DefaultEventPriority,
|
||||
DiscreteEventPriority,
|
||||
type EventPriority,
|
||||
} from 'react-reconciler/src/ReactEventPriorities';
|
||||
import {HostText} from 'react-reconciler/src/ReactWorkTags';
|
||||
|
||||
|
|
@ -317,7 +318,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function getCurrentEventPriority(): * {
|
||||
export function getCurrentEventPriority(): EventPriority {
|
||||
const currentEventPriority = fabricGetCurrentEventPriority
|
||||
? fabricGetCurrentEventPriority()
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ import {
|
|||
} from './ReactNativeComponentTree';
|
||||
import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
|
||||
|
||||
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
|
||||
import {
|
||||
DefaultEventPriority,
|
||||
type EventPriority,
|
||||
} from 'react-reconciler/src/ReactEventPriorities';
|
||||
|
||||
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
|
||||
|
||||
|
|
@ -217,9 +220,10 @@ export function getChildHostContext(
|
|||
}
|
||||
}
|
||||
|
||||
export function getPublicInstance(instance: Instance): * {
|
||||
export function getPublicInstance(instance: Instance): PublicInstance {
|
||||
// $FlowExpectedError[prop-missing] For compatibility with Fabric
|
||||
if (instance.canonical != null && instance.canonical.publicInstance != null) {
|
||||
// $FlowFixMe[incompatible-return]
|
||||
return instance.canonical.publicInstance;
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +266,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function getCurrentEventPriority(): * {
|
||||
export function getCurrentEventPriority(): EventPriority {
|
||||
return DefaultEventPriority;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,359 +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 {ReactNodeList} from 'shared/ReactTypes';
|
||||
|
||||
import type {
|
||||
Destination,
|
||||
Chunk,
|
||||
PrecomputedChunk,
|
||||
} from 'react-server/src/ReactServerStreamConfig';
|
||||
|
||||
import {
|
||||
writeChunk,
|
||||
writeChunkAndReturn,
|
||||
stringToChunk,
|
||||
stringToPrecomputedChunk,
|
||||
} from 'react-server/src/ReactServerStreamConfig';
|
||||
|
||||
export const isPrimaryRenderer = true;
|
||||
|
||||
// Every list of children or string is null terminated.
|
||||
const END_TAG = 0;
|
||||
// Tree node tags.
|
||||
const INSTANCE_TAG = 1;
|
||||
const PLACEHOLDER_TAG = 2;
|
||||
const SUSPENSE_PENDING_TAG = 3;
|
||||
const SUSPENSE_COMPLETE_TAG = 4;
|
||||
const SUSPENSE_CLIENT_RENDER_TAG = 5;
|
||||
// Command tags.
|
||||
const SEGMENT_TAG = 1;
|
||||
const SUSPENSE_UPDATE_TO_COMPLETE_TAG = 2;
|
||||
const SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG = 3;
|
||||
|
||||
const END = new Uint8Array(1);
|
||||
END[0] = END_TAG;
|
||||
const PLACEHOLDER = new Uint8Array(1);
|
||||
PLACEHOLDER[0] = PLACEHOLDER_TAG;
|
||||
const INSTANCE = new Uint8Array(1);
|
||||
INSTANCE[0] = INSTANCE_TAG;
|
||||
const SUSPENSE_PENDING = new Uint8Array(1);
|
||||
SUSPENSE_PENDING[0] = SUSPENSE_PENDING_TAG;
|
||||
const SUSPENSE_COMPLETE = new Uint8Array(1);
|
||||
SUSPENSE_COMPLETE[0] = SUSPENSE_COMPLETE_TAG;
|
||||
const SUSPENSE_CLIENT_RENDER = new Uint8Array(1);
|
||||
SUSPENSE_CLIENT_RENDER[0] = SUSPENSE_CLIENT_RENDER_TAG;
|
||||
|
||||
const SEGMENT = new Uint8Array(1);
|
||||
SEGMENT[0] = SEGMENT_TAG;
|
||||
const SUSPENSE_UPDATE_TO_COMPLETE = new Uint8Array(1);
|
||||
SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
|
||||
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
|
||||
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
|
||||
|
||||
export type Resources = void;
|
||||
export type BoundaryResources = void;
|
||||
|
||||
// Per response,
|
||||
export type ResponseState = {
|
||||
nextSuspenseID: number,
|
||||
};
|
||||
|
||||
// Allows us to keep track of what we've already written so we can refer back to it.
|
||||
export function createResponseState(): ResponseState {
|
||||
return {
|
||||
nextSuspenseID: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// isInAParentText
|
||||
export type FormatContext = boolean;
|
||||
|
||||
export function createRootFormatContext(): FormatContext {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getChildFormatContext(
|
||||
parentContext: FormatContext,
|
||||
type: string,
|
||||
props: Object,
|
||||
): FormatContext {
|
||||
const prevIsInAParentText = parentContext;
|
||||
const isInAParentText =
|
||||
type === 'AndroidTextInput' || // Android
|
||||
type === 'RCTMultilineTextInputView' || // iOS
|
||||
type === 'RCTSinglelineTextInputView' || // iOS
|
||||
type === 'RCTText' ||
|
||||
type === 'RCTVirtualText';
|
||||
|
||||
if (prevIsInAParentText !== isInAParentText) {
|
||||
return isInAParentText;
|
||||
} else {
|
||||
return parentContext;
|
||||
}
|
||||
}
|
||||
|
||||
// This object is used to lazily reuse the ID of the first generated node, or assign one.
|
||||
// This is very specific to DOM where we can't assign an ID to.
|
||||
export type SuspenseBoundaryID = number;
|
||||
|
||||
export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = -1;
|
||||
|
||||
export function assignSuspenseBoundaryID(
|
||||
responseState: ResponseState,
|
||||
): SuspenseBoundaryID {
|
||||
return responseState.nextSuspenseID++;
|
||||
}
|
||||
|
||||
export function makeId(
|
||||
responseState: ResponseState,
|
||||
treeId: string,
|
||||
localId: number,
|
||||
): string {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
|
||||
|
||||
export function pushTextInstance(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
text: string,
|
||||
responseState: ResponseState,
|
||||
// This Renderer does not use this argument
|
||||
textEmbedded: boolean,
|
||||
): boolean {
|
||||
target.push(
|
||||
INSTANCE,
|
||||
RAW_TEXT, // Type
|
||||
END, // Null terminated type string
|
||||
// TODO: props { text: text }
|
||||
END, // End of children
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function pushStartInstance(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
type: string,
|
||||
props: Object,
|
||||
resources: Resources,
|
||||
responseState: ResponseState,
|
||||
formatContext: FormatContext,
|
||||
textEmbedded: boolean,
|
||||
): ReactNodeList {
|
||||
target.push(
|
||||
INSTANCE,
|
||||
stringToChunk(type),
|
||||
END, // Null terminated type string
|
||||
// TODO: props
|
||||
);
|
||||
return props.children;
|
||||
}
|
||||
|
||||
export function pushEndInstance(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
type: string,
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
formatContext: FormatContext,
|
||||
): void {
|
||||
target.push(END);
|
||||
}
|
||||
|
||||
// In this Renderer this is a noop
|
||||
export function pushSegmentFinale(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
responseState: ResponseState,
|
||||
lastPushedText: boolean,
|
||||
textEmbedded: boolean,
|
||||
): void {}
|
||||
|
||||
export function writeCompletedRoot(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IDs are formatted as little endian Uint16
|
||||
function formatID(id: number): Uint8Array {
|
||||
if (id > 0xffff) {
|
||||
throw new Error(
|
||||
'More boundaries or placeholders than we expected to ever emit.',
|
||||
);
|
||||
}
|
||||
const buffer = new Uint8Array(2);
|
||||
buffer[0] = (id >>> 8) & 0xff;
|
||||
buffer[1] = id & 0xff;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Structural Nodes
|
||||
|
||||
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
|
||||
// display. It's never visible to users.
|
||||
export function writePlaceholder(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: number,
|
||||
): boolean {
|
||||
writeChunk(destination, PLACEHOLDER);
|
||||
return writeChunkAndReturn(destination, formatID(id));
|
||||
}
|
||||
|
||||
// Suspense boundaries are encoded as comments.
|
||||
export function writeStartCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, SUSPENSE_COMPLETE);
|
||||
}
|
||||
|
||||
export function pushStartCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
): void {
|
||||
target.push(SUSPENSE_COMPLETE);
|
||||
}
|
||||
|
||||
export function writeStartPendingSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
id: SuspenseBoundaryID,
|
||||
): boolean {
|
||||
writeChunk(destination, SUSPENSE_PENDING);
|
||||
return writeChunkAndReturn(destination, formatID(id));
|
||||
}
|
||||
export function writeStartClientRenderedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
// TODO: encode error for native
|
||||
errorDigest: ?string,
|
||||
errorMessage: ?string,
|
||||
errorComponentStack: ?string,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, SUSPENSE_CLIENT_RENDER);
|
||||
}
|
||||
export function writeEndCompletedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, END);
|
||||
}
|
||||
export function pushEndCompletedSuspenseBoundary(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
): void {
|
||||
target.push(END);
|
||||
}
|
||||
export function writeEndPendingSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, END);
|
||||
}
|
||||
export function writeEndClientRenderedSuspenseBoundary(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, END);
|
||||
}
|
||||
|
||||
export function writeStartSegment(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
formatContext: FormatContext,
|
||||
id: number,
|
||||
): boolean {
|
||||
writeChunk(destination, SEGMENT);
|
||||
return writeChunkAndReturn(destination, formatID(id));
|
||||
}
|
||||
export function writeEndSegment(
|
||||
destination: Destination,
|
||||
formatContext: FormatContext,
|
||||
): boolean {
|
||||
return writeChunkAndReturn(destination, END);
|
||||
}
|
||||
|
||||
// Instruction Set
|
||||
|
||||
export function writeCompletedSegmentInstruction(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
contentSegmentID: number,
|
||||
): boolean {
|
||||
// We don't need to emit this. Instead the client will keep track of pending placeholders.
|
||||
// TODO: Returning true here is not correct. Avoid having to call this function at all.
|
||||
return true;
|
||||
}
|
||||
|
||||
export function writeCompletedBoundaryInstruction(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
boundaryID: SuspenseBoundaryID,
|
||||
contentSegmentID: number,
|
||||
resources: BoundaryResources,
|
||||
): boolean {
|
||||
writeChunk(destination, SUSPENSE_UPDATE_TO_COMPLETE);
|
||||
writeChunk(destination, formatID(boundaryID));
|
||||
return writeChunkAndReturn(destination, formatID(contentSegmentID));
|
||||
}
|
||||
|
||||
export function writeClientRenderBoundaryInstruction(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
boundaryID: SuspenseBoundaryID,
|
||||
// TODO: encode error for native
|
||||
errorDigest: ?string,
|
||||
errorMessage: ?string,
|
||||
errorComponentStack: ?string,
|
||||
): boolean {
|
||||
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
|
||||
return writeChunkAndReturn(destination, formatID(boundaryID));
|
||||
}
|
||||
|
||||
export function writePreamble(
|
||||
destination: Destination,
|
||||
resources: Resources,
|
||||
responseState: ResponseState,
|
||||
willFlushAllSegments: boolean,
|
||||
) {}
|
||||
|
||||
export function writeHoistables(
|
||||
destination: Destination,
|
||||
resources: Resources,
|
||||
responseState: ResponseState,
|
||||
) {}
|
||||
|
||||
export function writePostamble(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
) {}
|
||||
|
||||
export function hoistResources(
|
||||
resources: Resources,
|
||||
boundaryResources: BoundaryResources,
|
||||
) {}
|
||||
|
||||
export function prepareHostDispatcher() {}
|
||||
export function createResources() {}
|
||||
export function createBoundaryResources() {}
|
||||
export function setCurrentlyRenderingBoundaryResourcesTarget(
|
||||
resources: Resources,
|
||||
boundaryResources: ?BoundaryResources,
|
||||
) {}
|
||||
|
||||
export function writeResourcesForBoundary(
|
||||
destination: Destination,
|
||||
boundaryResources: BoundaryResources,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export type TransitionStatus = mixed;
|
||||
export const NotPendingTransition: TransitionStatus = null;
|
||||
|
|
@ -1,19 +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 const isPrimaryRenderer = true;
|
||||
|
||||
export type Hints = null;
|
||||
export type HintModel = '';
|
||||
|
||||
export function createHints(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function prepareHostDispatcher() {}
|
||||
|
|
@ -55,6 +55,7 @@ import {
|
|||
enableLegacyHidden,
|
||||
enableHostSingletons,
|
||||
diffInCommitPhase,
|
||||
alwaysThrottleRetries,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
|
|
@ -2905,17 +2906,35 @@ function commitMutationEffectsOnFiber(
|
|||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
commitReconciliationEffects(finishedWork);
|
||||
|
||||
// TODO: We should mark a flag on the Suspense fiber itself, rather than
|
||||
// relying on the Offscreen fiber having a flag also being marked. The
|
||||
// reason is that this offscreen fiber might not be part of the work-in-
|
||||
// progress tree! It could have been reused from a previous render. This
|
||||
// doesn't lead to incorrect behavior because we don't rely on the flag
|
||||
// check alone; we also compare the states explicitly below. But for
|
||||
// modeling purposes, we _should_ be able to rely on the flag check alone.
|
||||
// So this is a bit fragile.
|
||||
//
|
||||
// Also, all this logic could/should move to the passive phase so it
|
||||
// doesn't block paint.
|
||||
const offscreenFiber: Fiber = (finishedWork.child: any);
|
||||
|
||||
if (offscreenFiber.flags & Visibility) {
|
||||
const newState: OffscreenState | null = offscreenFiber.memoizedState;
|
||||
const isHidden = newState !== null;
|
||||
if (isHidden) {
|
||||
const wasHidden =
|
||||
offscreenFiber.alternate !== null &&
|
||||
offscreenFiber.alternate.memoizedState !== null;
|
||||
if (!wasHidden) {
|
||||
// TODO: Move to passive phase
|
||||
// Throttle the appearance and disappearance of Suspense fallbacks.
|
||||
const isShowingFallback =
|
||||
(finishedWork.memoizedState: SuspenseState | null) !== null;
|
||||
const wasShowingFallback =
|
||||
current !== null &&
|
||||
(current.memoizedState: SuspenseState | null) !== null;
|
||||
|
||||
if (alwaysThrottleRetries) {
|
||||
if (isShowingFallback !== wasShowingFallback) {
|
||||
// A fallback is either appearing or disappearing.
|
||||
markCommitTimeOfFallback();
|
||||
}
|
||||
} else {
|
||||
if (isShowingFallback && !wasShowingFallback) {
|
||||
// Old behavior. Only mark when a fallback appears, not when
|
||||
// it disappears.
|
||||
markCommitTimeOfFallback();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ export const getNextHydratableSibling = shim;
|
|||
export const getFirstHydratableChild = shim;
|
||||
export const getFirstHydratableChildWithinContainer = shim;
|
||||
export const getFirstHydratableChildWithinSuspenseInstance = shim;
|
||||
export const shouldSkipHydratableForInstance = shim;
|
||||
export const shouldSkipHydratableForTextInstance = shim;
|
||||
export const shouldSkipHydratableForSuspenseInstance = shim;
|
||||
export const canHydrateInstance = shim;
|
||||
export const canHydrateTextInstance = shim;
|
||||
export const canHydrateSuspenseInstance = shim;
|
||||
|
|
|
|||
|
|
@ -149,11 +149,13 @@ import type {ThenableState} from './ReactFiberThenable';
|
|||
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
|
||||
import {requestAsyncActionContext} from './ReactFiberAsyncAction';
|
||||
import {HostTransitionContext} from './ReactFiberHostContext';
|
||||
import {requestTransitionLane} from './ReactFiberRootScheduler';
|
||||
|
||||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
|
||||
|
||||
export type Update<S, A> = {
|
||||
lane: Lane,
|
||||
revertLane: Lane,
|
||||
action: A,
|
||||
hasEagerState: boolean,
|
||||
eagerState: S | null,
|
||||
|
|
@ -1136,6 +1138,14 @@ function updateReducer<S, I, A>(
|
|||
init?: I => S,
|
||||
): [S, Dispatch<A>] {
|
||||
const hook = updateWorkInProgressHook();
|
||||
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
|
||||
}
|
||||
|
||||
function updateReducerImpl<S, A>(
|
||||
hook: Hook,
|
||||
current: Hook,
|
||||
reducer: (S, A) => S,
|
||||
): [S, Dispatch<A>] {
|
||||
const queue = hook.queue;
|
||||
|
||||
if (queue === null) {
|
||||
|
|
@ -1146,10 +1156,8 @@ function updateReducer<S, I, A>(
|
|||
|
||||
queue.lastRenderedReducer = reducer;
|
||||
|
||||
const current: Hook = (currentHook: any);
|
||||
|
||||
// The last rebase update that is NOT part of the base state.
|
||||
let baseQueue = current.baseQueue;
|
||||
let baseQueue = hook.baseQueue;
|
||||
|
||||
// The last pending update that hasn't been processed yet.
|
||||
const pendingQueue = queue.pending;
|
||||
|
|
@ -1180,7 +1188,7 @@ function updateReducer<S, I, A>(
|
|||
if (baseQueue !== null) {
|
||||
// We have a queue to process.
|
||||
const first = baseQueue.next;
|
||||
let newState = current.baseState;
|
||||
let newState = hook.baseState;
|
||||
|
||||
let newBaseState = null;
|
||||
let newBaseQueueFirst = null;
|
||||
|
|
@ -1206,6 +1214,7 @@ function updateReducer<S, I, A>(
|
|||
// update/state.
|
||||
const clone: Update<S, A> = {
|
||||
lane: updateLane,
|
||||
revertLane: update.revertLane,
|
||||
action: update.action,
|
||||
hasEagerState: update.hasEagerState,
|
||||
eagerState: update.eagerState,
|
||||
|
|
@ -1228,18 +1237,68 @@ function updateReducer<S, I, A>(
|
|||
} else {
|
||||
// This update does have sufficient priority.
|
||||
|
||||
if (newBaseQueueLast !== null) {
|
||||
const clone: Update<S, A> = {
|
||||
// This update is going to be committed so we never want uncommit
|
||||
// it. Using NoLane works because 0 is a subset of all bitmasks, so
|
||||
// this will never be skipped by the check above.
|
||||
lane: NoLane,
|
||||
action: update.action,
|
||||
hasEagerState: update.hasEagerState,
|
||||
eagerState: update.eagerState,
|
||||
next: (null: any),
|
||||
};
|
||||
newBaseQueueLast = newBaseQueueLast.next = clone;
|
||||
// Check if this is an optimistic update.
|
||||
const revertLane = update.revertLane;
|
||||
if (!enableAsyncActions || revertLane === NoLane) {
|
||||
// This is not an optimistic update, and we're going to apply it now.
|
||||
// But, if there were earlier updates that were skipped, we need to
|
||||
// leave this update in the queue so it can be rebased later.
|
||||
if (newBaseQueueLast !== null) {
|
||||
const clone: Update<S, A> = {
|
||||
// This update is going to be committed so we never want uncommit
|
||||
// it. Using NoLane works because 0 is a subset of all bitmasks, so
|
||||
// this will never be skipped by the check above.
|
||||
lane: NoLane,
|
||||
revertLane: NoLane,
|
||||
action: update.action,
|
||||
hasEagerState: update.hasEagerState,
|
||||
eagerState: update.eagerState,
|
||||
next: (null: any),
|
||||
};
|
||||
newBaseQueueLast = newBaseQueueLast.next = clone;
|
||||
}
|
||||
} else {
|
||||
// This is an optimistic update. If the "revert" priority is
|
||||
// sufficient, don't apply the update. Otherwise, apply the update,
|
||||
// but leave it in the queue so it can be either reverted or
|
||||
// rebased in a subsequent render.
|
||||
if (isSubsetOfLanes(renderLanes, revertLane)) {
|
||||
// The transition that this optimistic update is associated with
|
||||
// has finished. Pretend the update doesn't exist by skipping
|
||||
// over it.
|
||||
update = update.next;
|
||||
continue;
|
||||
} else {
|
||||
const clone: Update<S, A> = {
|
||||
// Once we commit an optimistic update, we shouldn't uncommit it
|
||||
// until the transition it is associated with has finished
|
||||
// (represented by revertLane). Using NoLane here works because 0
|
||||
// is a subset of all bitmasks, so this will never be skipped by
|
||||
// the check above.
|
||||
lane: NoLane,
|
||||
// Reuse the same revertLane so we know when the transition
|
||||
// has finished.
|
||||
revertLane: update.revertLane,
|
||||
action: update.action,
|
||||
hasEagerState: update.hasEagerState,
|
||||
eagerState: update.eagerState,
|
||||
next: (null: any),
|
||||
};
|
||||
if (newBaseQueueLast === null) {
|
||||
newBaseQueueFirst = newBaseQueueLast = clone;
|
||||
newBaseState = newState;
|
||||
} else {
|
||||
newBaseQueueLast = newBaseQueueLast.next = clone;
|
||||
}
|
||||
// Update the remaining priority in the queue.
|
||||
// TODO: Don't need to accumulate this. Instead, we can remove
|
||||
// renderLanes from the original lanes.
|
||||
currentlyRenderingFiber.lanes = mergeLanes(
|
||||
currentlyRenderingFiber.lanes,
|
||||
revertLane,
|
||||
);
|
||||
markSkippedUpdateLanes(revertLane);
|
||||
}
|
||||
}
|
||||
|
||||
// Process this update.
|
||||
|
|
@ -1717,8 +1776,6 @@ function mountSyncExternalStore<T>(
|
|||
// clean-up function, and we track the deps correctly, we can call pushEffect
|
||||
// directly, without storing any additional state. For the same reason, we
|
||||
// don't need to set a static flag, either.
|
||||
// TODO: We can move this to the passive phase once we add a pre-commit
|
||||
// consistency check. See the next comment.
|
||||
fiber.flags |= PassiveEffect;
|
||||
pushEffect(
|
||||
HookHasEffect | HookPassive,
|
||||
|
|
@ -1740,15 +1797,28 @@ function updateSyncExternalStore<T>(
|
|||
// Read the current snapshot from the store on every render. This breaks the
|
||||
// normal rules of React, and only works because store updates are
|
||||
// always synchronous.
|
||||
const nextSnapshot = getSnapshot();
|
||||
if (__DEV__) {
|
||||
if (!didWarnUncachedGetSnapshot) {
|
||||
const cachedSnapshot = getSnapshot();
|
||||
if (!is(nextSnapshot, cachedSnapshot)) {
|
||||
console.error(
|
||||
'The result of getSnapshot should be cached to avoid an infinite loop',
|
||||
);
|
||||
didWarnUncachedGetSnapshot = true;
|
||||
let nextSnapshot;
|
||||
const isHydrating = getIsHydrating();
|
||||
if (isHydrating) {
|
||||
// Needed for strict mode double render
|
||||
if (getServerSnapshot === undefined) {
|
||||
throw new Error(
|
||||
'Missing getServerSnapshot, which is required for ' +
|
||||
'server-rendered content. Will revert to client rendering.',
|
||||
);
|
||||
}
|
||||
nextSnapshot = getServerSnapshot();
|
||||
} else {
|
||||
nextSnapshot = getSnapshot();
|
||||
if (__DEV__) {
|
||||
if (!didWarnUncachedGetSnapshot) {
|
||||
const cachedSnapshot = getSnapshot();
|
||||
if (!is(nextSnapshot, cachedSnapshot)) {
|
||||
console.error(
|
||||
'The result of getSnapshot should be cached to avoid an infinite loop',
|
||||
);
|
||||
didWarnUncachedGetSnapshot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1771,7 +1841,7 @@ function updateSyncExternalStore<T>(
|
|||
if (
|
||||
inst.getSnapshot !== getSnapshot ||
|
||||
snapshotChanged ||
|
||||
// Check if the susbcribe function changed. We can save some memory by
|
||||
// Check if the subscribe function changed. We can save some memory by
|
||||
// checking whether we scheduled a subscription effect above.
|
||||
(workInProgressHook !== null &&
|
||||
workInProgressHook.memoizedState.tag & HookHasEffect)
|
||||
|
|
@ -1795,7 +1865,7 @@ function updateSyncExternalStore<T>(
|
|||
);
|
||||
}
|
||||
|
||||
if (!includesBlockingLane(root, renderLanes)) {
|
||||
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
|
||||
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
|
||||
}
|
||||
}
|
||||
|
|
@ -1884,9 +1954,7 @@ function forceStoreRerender(fiber: Fiber) {
|
|||
}
|
||||
}
|
||||
|
||||
function mountState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
|
||||
const hook = mountWorkInProgressHook();
|
||||
if (typeof initialState === 'function') {
|
||||
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
|
||||
|
|
@ -1901,21 +1969,106 @@ function mountState<S>(
|
|||
lastRenderedState: (initialState: any),
|
||||
};
|
||||
hook.queue = queue;
|
||||
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
|
||||
(dispatchSetState.bind(null, currentlyRenderingFiber, queue): any));
|
||||
return hook;
|
||||
}
|
||||
|
||||
function mountState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
const hook = mountStateImpl(initialState);
|
||||
const queue = hook.queue;
|
||||
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
|
||||
null,
|
||||
currentlyRenderingFiber,
|
||||
queue,
|
||||
): any);
|
||||
queue.dispatch = dispatch;
|
||||
return [hook.memoizedState, dispatch];
|
||||
}
|
||||
|
||||
function updateState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
return updateReducer(basicStateReducer, (initialState: any));
|
||||
return updateReducer(basicStateReducer, initialState);
|
||||
}
|
||||
|
||||
function rerenderState<S>(
|
||||
initialState: (() => S) | S,
|
||||
): [S, Dispatch<BasicStateAction<S>>] {
|
||||
return rerenderReducer(basicStateReducer, (initialState: any));
|
||||
return rerenderReducer(basicStateReducer, initialState);
|
||||
}
|
||||
|
||||
function mountOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
const hook = mountWorkInProgressHook();
|
||||
hook.memoizedState = hook.baseState = passthrough;
|
||||
const queue: UpdateQueue<S, A> = {
|
||||
pending: null,
|
||||
lanes: NoLanes,
|
||||
dispatch: null,
|
||||
// Optimistic state does not use the eager update optimization.
|
||||
lastRenderedReducer: null,
|
||||
lastRenderedState: null,
|
||||
};
|
||||
hook.queue = queue;
|
||||
// This is different than the normal setState function.
|
||||
const dispatch: A => void = (dispatchOptimisticSetState.bind(
|
||||
null,
|
||||
currentlyRenderingFiber,
|
||||
true,
|
||||
queue,
|
||||
): any);
|
||||
queue.dispatch = dispatch;
|
||||
return [passthrough, dispatch];
|
||||
}
|
||||
|
||||
function updateOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
const hook = updateWorkInProgressHook();
|
||||
|
||||
// Optimistic updates are always rebased on top of the latest value passed in
|
||||
// as an argument. It's called a passthrough because if there are no pending
|
||||
// updates, it will be returned as-is.
|
||||
//
|
||||
// Reset the base state and memoized state to the passthrough. Future
|
||||
// updates will be applied on top of this.
|
||||
hook.baseState = hook.memoizedState = passthrough;
|
||||
|
||||
// If a reducer is not provided, default to the same one used by useState.
|
||||
const resolvedReducer: (S, A) => S =
|
||||
typeof reducer === 'function' ? reducer : (basicStateReducer: any);
|
||||
|
||||
return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);
|
||||
}
|
||||
|
||||
function rerenderOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
// Unlike useState, useOptimistic doesn't support render phase updates.
|
||||
// Also unlike useState, we need to replay all pending updates again in case
|
||||
// the passthrough value changed.
|
||||
//
|
||||
// So instead of a forked re-render implementation that knows how to handle
|
||||
// render phase udpates, we can use the same implementation as during a
|
||||
// regular mount or update.
|
||||
|
||||
if (currentHook !== null) {
|
||||
// This is an update. Process the update queue.
|
||||
return updateOptimistic(passthrough, reducer);
|
||||
}
|
||||
|
||||
// This is a mount. No updates to process.
|
||||
const hook = updateWorkInProgressHook();
|
||||
// Reset the base state and memoized state to the passthrough. Future
|
||||
// updates will be applied on top of this.
|
||||
hook.baseState = hook.memoizedState = passthrough;
|
||||
const dispatch = hook.queue.dispatch;
|
||||
return [passthrough, dispatch];
|
||||
}
|
||||
|
||||
function pushEffect(
|
||||
|
|
@ -2075,7 +2228,8 @@ function updateEffectImpl(
|
|||
const effect: Effect = hook.memoizedState;
|
||||
const inst = effect.inst;
|
||||
|
||||
// currentHook is null when rerendering after a render phase state update.
|
||||
// currentHook is null on initial mount when rerendering after a render phase
|
||||
// state update or for strict mode.
|
||||
if (currentHook !== null) {
|
||||
if (nextDeps !== null) {
|
||||
const prevEffect: Effect = currentHook.memoizedState;
|
||||
|
|
@ -2445,9 +2599,10 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
|
|||
}
|
||||
|
||||
function startTransition<S>(
|
||||
fiber: Fiber,
|
||||
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
|
||||
pendingState: S,
|
||||
finishedState: S,
|
||||
setPending: (Thenable<S> | S) => void,
|
||||
callback: () => mixed,
|
||||
options?: StartTransitionOptions,
|
||||
): void {
|
||||
|
|
@ -2457,8 +2612,20 @@ function startTransition<S>(
|
|||
);
|
||||
|
||||
const prevTransition = ReactCurrentBatchConfig.transition;
|
||||
ReactCurrentBatchConfig.transition = null;
|
||||
setPending(pendingState);
|
||||
|
||||
if (enableAsyncActions) {
|
||||
// We don't really need to use an optimistic update here, because we
|
||||
// schedule a second "revert" update below (which we use to suspend the
|
||||
// transition until the async action scope has finished). But we'll use an
|
||||
// optimistic update anyway to make it less likely the behavior accidentally
|
||||
// diverges; for example, both an optimistic update and this one should
|
||||
// share the same lane.
|
||||
dispatchOptimisticSetState(fiber, false, queue, pendingState);
|
||||
} else {
|
||||
ReactCurrentBatchConfig.transition = null;
|
||||
dispatchSetState(fiber, queue, pendingState);
|
||||
}
|
||||
|
||||
const currentTransition = (ReactCurrentBatchConfig.transition =
|
||||
({}: BatchConfigTransition));
|
||||
|
||||
|
|
@ -2485,10 +2652,10 @@ function startTransition<S>(
|
|||
returnValue,
|
||||
finishedState,
|
||||
);
|
||||
setPending(maybeThenable);
|
||||
dispatchSetState(fiber, queue, maybeThenable);
|
||||
} else {
|
||||
// Async actions are not enabled.
|
||||
setPending(finishedState);
|
||||
dispatchSetState(fiber, queue, finishedState);
|
||||
callback();
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -2501,7 +2668,7 @@ function startTransition<S>(
|
|||
status: 'rejected',
|
||||
reason: error,
|
||||
};
|
||||
setPending(rejectedThenable);
|
||||
dispatchSetState(fiber, queue, rejectedThenable);
|
||||
} else {
|
||||
// The error rethrowing behavior is only enabled when the async actions
|
||||
// feature is on, even for sync actions.
|
||||
|
|
@ -2553,7 +2720,10 @@ export function startHostTransition<F>(
|
|||
);
|
||||
}
|
||||
|
||||
let setPending;
|
||||
let queue: UpdateQueue<
|
||||
Thenable<TransitionStatus> | TransitionStatus,
|
||||
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
|
||||
>;
|
||||
if (formFiber.memoizedState === null) {
|
||||
// Upgrade this host component fiber to be stateful. We're going to pretend
|
||||
// it was stateful all along so we can reuse most of the implementation
|
||||
|
|
@ -2561,28 +2731,28 @@ export function startHostTransition<F>(
|
|||
//
|
||||
// Create the state hook used by TransitionAwareHostComponent. This is
|
||||
// essentially an inlined version of mountState.
|
||||
const queue: UpdateQueue<
|
||||
Thenable<TransitionStatus> | TransitionStatus,
|
||||
const newQueue: UpdateQueue<
|
||||
Thenable<TransitionStatus> | TransitionStatus,
|
||||
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
|
||||
> = {
|
||||
pending: null,
|
||||
lanes: NoLanes,
|
||||
dispatch: null,
|
||||
// We're going to cheat and intentionally not create a bound dispatch
|
||||
// method, because we can call it directly in startTransition.
|
||||
dispatch: (null: any),
|
||||
lastRenderedReducer: basicStateReducer,
|
||||
lastRenderedState: NoPendingHostTransition,
|
||||
};
|
||||
queue = newQueue;
|
||||
|
||||
const stateHook: Hook = {
|
||||
memoizedState: NoPendingHostTransition,
|
||||
baseState: NoPendingHostTransition,
|
||||
baseQueue: null,
|
||||
queue: queue,
|
||||
queue: newQueue,
|
||||
next: null,
|
||||
};
|
||||
|
||||
const dispatch: (Thenable<TransitionStatus> | TransitionStatus) => void =
|
||||
(dispatchSetState.bind(null, formFiber, queue): any);
|
||||
setPending = queue.dispatch = dispatch;
|
||||
|
||||
// Add the state hook to both fiber alternates. The idea is that the fiber
|
||||
// had this hook all along.
|
||||
formFiber.memoizedState = stateHook;
|
||||
|
|
@ -2593,15 +2763,14 @@ export function startHostTransition<F>(
|
|||
} else {
|
||||
// This fiber was already upgraded to be stateful.
|
||||
const stateHook: Hook = formFiber.memoizedState;
|
||||
const dispatch: (Thenable<TransitionStatus> | TransitionStatus) => void =
|
||||
stateHook.queue.dispatch;
|
||||
setPending = dispatch;
|
||||
queue = stateHook.queue;
|
||||
}
|
||||
|
||||
startTransition(
|
||||
formFiber,
|
||||
queue,
|
||||
pendingState,
|
||||
NoPendingHostTransition,
|
||||
setPending,
|
||||
// TODO: We can avoid this extra wrapper, somehow. Figure out layering
|
||||
// once more of this function is implemented.
|
||||
() => callback(formData),
|
||||
|
|
@ -2612,9 +2781,15 @@ function mountTransition(): [
|
|||
boolean,
|
||||
(callback: () => void, options?: StartTransitionOptions) => void,
|
||||
] {
|
||||
const [, setPending] = mountState((false: Thenable<boolean> | boolean));
|
||||
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
|
||||
// The `start` method never changes.
|
||||
const start = startTransition.bind(null, true, false, setPending);
|
||||
const start = startTransition.bind(
|
||||
null,
|
||||
currentlyRenderingFiber,
|
||||
stateHook.queue,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
const hook = mountWorkInProgressHook();
|
||||
hook.memoizedState = start;
|
||||
return [false, start];
|
||||
|
|
@ -2785,6 +2960,7 @@ function dispatchReducerAction<S, A>(
|
|||
|
||||
const update: Update<S, A> = {
|
||||
lane,
|
||||
revertLane: NoLane,
|
||||
action,
|
||||
hasEagerState: false,
|
||||
eagerState: null,
|
||||
|
|
@ -2823,6 +2999,7 @@ function dispatchSetState<S, A>(
|
|||
|
||||
const update: Update<S, A> = {
|
||||
lane,
|
||||
revertLane: NoLane,
|
||||
action,
|
||||
hasEagerState: false,
|
||||
eagerState: null,
|
||||
|
|
@ -2886,6 +3063,58 @@ function dispatchSetState<S, A>(
|
|||
markUpdateInDevTools(fiber, lane, action);
|
||||
}
|
||||
|
||||
function dispatchOptimisticSetState<S, A>(
|
||||
fiber: Fiber,
|
||||
throwIfDuringRender: boolean,
|
||||
queue: UpdateQueue<S, A>,
|
||||
action: A,
|
||||
): void {
|
||||
const update: Update<S, A> = {
|
||||
// An optimistic update commits synchronously.
|
||||
lane: SyncLane,
|
||||
// After committing, the optimistic update is "reverted" using the same
|
||||
// lane as the transition it's associated with.
|
||||
//
|
||||
// TODO: Warn if there's no transition/action associated with this
|
||||
// optimistic update.
|
||||
revertLane: requestTransitionLane(),
|
||||
action,
|
||||
hasEagerState: false,
|
||||
eagerState: null,
|
||||
next: (null: any),
|
||||
};
|
||||
|
||||
if (isRenderPhaseUpdate(fiber)) {
|
||||
// When calling startTransition during render, this warns instead of
|
||||
// throwing because throwing would be a breaking change. setOptimisticState
|
||||
// is a new API so it's OK to throw.
|
||||
if (throwIfDuringRender) {
|
||||
throw new Error('Cannot update optimistic state while rendering.');
|
||||
} else {
|
||||
// startTransition was called during render. We don't need to do anything
|
||||
// besides warn here because the render phase update would be overidden by
|
||||
// the second update, anyway. We can remove this branch and make it throw
|
||||
// in a future release.
|
||||
if (__DEV__) {
|
||||
console.error('Cannot call startTransition while rendering.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
|
||||
if (root !== null) {
|
||||
// NOTE: The optimistic update implementation assumes that the transition
|
||||
// will never be attempted before the optimistic update. This currently
|
||||
// holds because the optimistic update is always synchronous. If we ever
|
||||
// change that, we'll need to account for this.
|
||||
scheduleUpdateOnFiber(root, fiber, SyncLane);
|
||||
// Optimistic updates are always synchronous, so we don't need to call
|
||||
// entangleTransitionUpdate here.
|
||||
}
|
||||
}
|
||||
|
||||
markUpdateInDevTools(fiber, SyncLane, action);
|
||||
}
|
||||
|
||||
function isRenderPhaseUpdate(fiber: Fiber): boolean {
|
||||
const alternate = fiber.alternate;
|
||||
return (
|
||||
|
|
@ -2989,6 +3218,9 @@ if (enableFormActions && enableAsyncActions) {
|
|||
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
|
||||
throwInvalidHookError;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnMount: Dispatcher = {
|
||||
readContext,
|
||||
|
|
@ -3024,6 +3256,10 @@ if (enableFormActions && enableAsyncActions) {
|
|||
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
readContext,
|
||||
|
||||
|
|
@ -3058,6 +3294,9 @@ if (enableFormActions && enableAsyncActions) {
|
|||
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnRerender: Dispatcher = {
|
||||
readContext,
|
||||
|
|
@ -3093,6 +3332,9 @@ if (enableFormActions && enableAsyncActions) {
|
|||
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic;
|
||||
}
|
||||
|
||||
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
|
||||
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
|
||||
|
|
@ -3283,6 +3525,17 @@ if (__DEV__) {
|
|||
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
mountHookTypesDev();
|
||||
return mountOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnMountWithHookTypesInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -3441,6 +3694,17 @@ if (__DEV__) {
|
|||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
updateHookTypesDev();
|
||||
return mountOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -3601,6 +3865,17 @@ if (__DEV__) {
|
|||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
updateHookTypesDev();
|
||||
return updateOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -3761,6 +4036,17 @@ if (__DEV__) {
|
|||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
updateHookTypesDev();
|
||||
return rerenderOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnMountInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -3943,6 +4229,18 @@ if (__DEV__) {
|
|||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -4128,6 +4426,18 @@ if (__DEV__) {
|
|||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
|
|
@ -4313,4 +4623,16 @@ if (__DEV__) {
|
|||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
|
||||
useHostTransitionStatus;
|
||||
}
|
||||
if (enableAsyncActions) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic =
|
||||
function useOptimistic<S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
): [S, (A) => void] {
|
||||
currentHookNameInDev = 'useOptimistic';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return rerenderOptimistic(passthrough, reducer);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,9 +74,6 @@ import {
|
|||
didNotFindHydratableTextInstance,
|
||||
didNotFindHydratableSuspenseInstance,
|
||||
resolveSingletonInstance,
|
||||
shouldSkipHydratableForInstance,
|
||||
shouldSkipHydratableForTextInstance,
|
||||
shouldSkipHydratableForSuspenseInstance,
|
||||
canHydrateInstance,
|
||||
canHydrateTextInstance,
|
||||
canHydrateSuspenseInstance,
|
||||
|
|
@ -355,6 +352,7 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
|
|||
nextInstance,
|
||||
fiber.type,
|
||||
fiber.pendingProps,
|
||||
rootOrSingletonContext,
|
||||
);
|
||||
if (instance !== null) {
|
||||
fiber.stateNode = (instance: Instance);
|
||||
|
|
@ -369,7 +367,11 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
|
|||
function tryHydrateText(fiber: Fiber, nextInstance: any) {
|
||||
// fiber is a HostText Fiber
|
||||
const text = fiber.pendingProps;
|
||||
const textInstance = canHydrateTextInstance(nextInstance, text);
|
||||
const textInstance = canHydrateTextInstance(
|
||||
nextInstance,
|
||||
text,
|
||||
rootOrSingletonContext,
|
||||
);
|
||||
if (textInstance !== null) {
|
||||
fiber.stateNode = (textInstance: TextInstance);
|
||||
hydrationParentFiber = fiber;
|
||||
|
|
@ -382,7 +384,10 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
|
|||
|
||||
function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
|
||||
// fiber is a SuspenseComponent Fiber
|
||||
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
|
||||
const suspenseInstance = canHydrateSuspenseInstance(
|
||||
nextInstance,
|
||||
rootOrSingletonContext,
|
||||
);
|
||||
if (suspenseInstance !== null) {
|
||||
const suspenseState: SuspenseState = {
|
||||
dehydrated: suspenseInstance,
|
||||
|
|
@ -441,44 +446,6 @@ function claimHydratableSingleton(fiber: Fiber): void {
|
|||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttemptableInstance(fiber: Fiber) {
|
||||
// fiber is HostComponent Fiber
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForInstance(
|
||||
nextHydratableInstance,
|
||||
fiber.type,
|
||||
fiber.pendingProps,
|
||||
)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttemptableTextInstance() {
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForTextInstance(nextHydratableInstance)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttemptableSuspenseInstance() {
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
||||
if (!isHydrating) {
|
||||
return;
|
||||
|
|
@ -493,10 +460,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
|||
}
|
||||
}
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttemptableInstance(fiber);
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
|
|
@ -521,10 +484,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
|||
// might be flawed or unnecessary.
|
||||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttemptableInstance(fiber);
|
||||
}
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateInstance(fiber, nextHydratableInstance)
|
||||
|
|
@ -552,12 +511,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
|
|||
const isHydratable = isHydratableText(text);
|
||||
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext && isHydratable) {
|
||||
// We may need to skip past certain nodes in these contexts.
|
||||
// We don't skip if the text is not hydratable because we know no hydratables
|
||||
// exist which could match this Fiber
|
||||
advanceToFirstAttemptableTextInstance();
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance || !isHydratable) {
|
||||
// We exclude non hydrabable text because we know there are no matching hydratables.
|
||||
|
|
@ -585,11 +538,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
|
|||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
|
||||
if (rootOrSingletonContext && isHydratable) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttemptableTextInstance();
|
||||
}
|
||||
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateText(fiber, nextHydratableInstance)
|
||||
|
|
@ -614,10 +562,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
|
|||
return;
|
||||
}
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttemptableSuspenseInstance();
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
|
|
@ -643,11 +587,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
|
|||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttemptableSuspenseInstance();
|
||||
}
|
||||
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateSuspense(fiber, nextHydratableInstance)
|
||||
|
|
@ -863,7 +802,8 @@ function popHydrationState(fiber: Fiber): boolean {
|
|||
fiber.tag !== HostSingleton &&
|
||||
!(
|
||||
fiber.tag === HostComponent &&
|
||||
shouldSetTextContent(fiber.type, fiber.memoizedProps)
|
||||
(!shouldDeleteUnhydratedTailInstances(fiber.type) ||
|
||||
shouldSetTextContent(fiber.type, fiber.memoizedProps))
|
||||
)
|
||||
) {
|
||||
shouldClear = true;
|
||||
|
|
|
|||
|
|
@ -151,17 +151,29 @@ export function flushSyncWorkOnLegacyRootsOnly() {
|
|||
flushSyncWorkAcrossRoots_impl(true);
|
||||
}
|
||||
|
||||
export function _doFlushWork(
|
||||
firstRoot,
|
||||
workInProgressRoot,
|
||||
workInProgressRootRenderLanes,
|
||||
onlyLegacy,
|
||||
) {
|
||||
let didPerformSomeWork = false;
|
||||
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
|
||||
if (isFlushingWork) {
|
||||
// Prevent reentrancy.
|
||||
// TODO: Is this overly defensive? The callers must check the execution
|
||||
// context first regardless.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mightHavePendingSyncWork) {
|
||||
// Fast path. There's no sync work to do.
|
||||
return;
|
||||
}
|
||||
|
||||
const workInProgressRoot = getWorkInProgressRoot();
|
||||
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
|
||||
|
||||
// There may or may not be synchronous work scheduled. Let's check.
|
||||
let didPerformSomeWork;
|
||||
let errors: Array<mixed> | null = null;
|
||||
isFlushingWork = true;
|
||||
do {
|
||||
didPerformSomeWork = false;
|
||||
let root = firstRoot;
|
||||
let root = firstScheduledRoot;
|
||||
while (root !== null) {
|
||||
if (onlyLegacy && root.tag !== LegacyRoot) {
|
||||
// Skip non-legacy roots.
|
||||
|
|
@ -190,33 +202,6 @@ export function _doFlushWork(
|
|||
root = root.next;
|
||||
}
|
||||
} while (didPerformSomeWork);
|
||||
|
||||
return errors;
|
||||
}
|
||||
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
|
||||
if (isFlushingWork) {
|
||||
// Prevent reentrancy.
|
||||
// TODO: Is this overly defensive? The callers must check the execution
|
||||
// context first regardless.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mightHavePendingSyncWork) {
|
||||
// Fast path. There's no sync work to do.
|
||||
return;
|
||||
}
|
||||
|
||||
const workInProgressRoot = getWorkInProgressRoot();
|
||||
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
|
||||
|
||||
// There may or may not be synchronous work scheduled. Let's check.
|
||||
isFlushingWork = true;
|
||||
const errors = _doFlushWork(
|
||||
firstScheduledRoot,
|
||||
workInProgressRoot,
|
||||
workInProgressRootRenderLanes,
|
||||
onlyLegacy,
|
||||
);
|
||||
isFlushingWork = false;
|
||||
|
||||
// If any errors were thrown, rethrow them right before exiting.
|
||||
|
|
|
|||
|
|
@ -370,10 +370,12 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
|
|||
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
|
||||
null;
|
||||
|
||||
// The most recent time we committed a fallback. This lets us ensure a train
|
||||
// model where we don't commit new loading states in too quick succession.
|
||||
// The most recent time we either committed a fallback, or when a fallback was
|
||||
// filled in with the resolved UI. This lets us throttle the appearance of new
|
||||
// content as it streams in, to minimize jank.
|
||||
// TODO: Think of a better name for this variable?
|
||||
let globalMostRecentFallbackTime: number = 0;
|
||||
const FALLBACK_THROTTLE_MS: number = 500;
|
||||
const FALLBACK_THROTTLE_MS: number = 300;
|
||||
|
||||
// The absolute time for when we should start giving up on rendering
|
||||
// more and prefer CPU suspense heuristics instead.
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ export type HookType =
|
|||
| 'useMutableSource'
|
||||
| 'useSyncExternalStore'
|
||||
| 'useId'
|
||||
| 'useCacheRefresh';
|
||||
| 'useCacheRefresh'
|
||||
| 'useOptimistic';
|
||||
|
||||
export type ContextDependency<T> = {
|
||||
context: ReactContext<T>,
|
||||
|
|
@ -423,6 +424,10 @@ export type Dispatcher = {
|
|||
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
|
||||
useMemoCache?: (size: number) => Array<any>,
|
||||
useHostTransitionStatus?: () => TransitionStatus,
|
||||
useOptimistic?: <S, A>(
|
||||
passthrough: S,
|
||||
reducer: ?(S, A) => S,
|
||||
) => [S, (A) => void],
|
||||
};
|
||||
|
||||
export type CacheDispatcher = {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ let act;
|
|||
let assertLog;
|
||||
let useTransition;
|
||||
let useState;
|
||||
let useOptimistic;
|
||||
let textCache;
|
||||
|
||||
describe('ReactAsyncActions', () => {
|
||||
|
|
@ -18,6 +19,7 @@ describe('ReactAsyncActions', () => {
|
|||
assertLog = require('internal-test-utils').assertLog;
|
||||
useTransition = React.useTransition;
|
||||
useState = React.useState;
|
||||
useOptimistic = React.experimental_useOptimistic;
|
||||
|
||||
textCache = new Map();
|
||||
});
|
||||
|
|
@ -644,4 +646,432 @@ describe('ReactAsyncActions', () => {
|
|||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useOptimistic can be used to implement a pending state', async () => {
|
||||
const startTransition = React.startTransition;
|
||||
|
||||
let setIsPending;
|
||||
function App({text}) {
|
||||
const [isPending, _setIsPending] = useOptimistic(false);
|
||||
setIsPending = _setIsPending;
|
||||
return (
|
||||
<>
|
||||
<Text text={'Pending: ' + isPending} />
|
||||
<AsyncText text={text} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
const root = ReactNoop.createRoot();
|
||||
resolveText('A');
|
||||
await act(() => root.render(<App text="A" />));
|
||||
assertLog(['Pending: false', 'A']);
|
||||
expect(root).toMatchRenderedOutput('Pending: falseA');
|
||||
|
||||
// Start a transition
|
||||
await act(() =>
|
||||
startTransition(() => {
|
||||
setIsPending(true);
|
||||
root.render(<App text="B" />);
|
||||
}),
|
||||
);
|
||||
assertLog([
|
||||
// Render the pending state immediately
|
||||
'Pending: true',
|
||||
'A',
|
||||
|
||||
// Then attempt to render the transition. The pending state will be
|
||||
// automatically reverted.
|
||||
'Pending: false',
|
||||
'Suspend! [B]',
|
||||
]);
|
||||
|
||||
// Resolve the transition
|
||||
await act(() => resolveText('B'));
|
||||
assertLog([
|
||||
// Render the pending state immediately
|
||||
'Pending: false',
|
||||
'B',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useOptimistic rebases pending updates on top of passthrough value', async () => {
|
||||
let serverCart = ['A'];
|
||||
|
||||
async function submitNewItem(item) {
|
||||
await getText('Adding item ' + item);
|
||||
serverCart = [...serverCart, item];
|
||||
React.startTransition(() => {
|
||||
root.render(<App cart={serverCart} />);
|
||||
});
|
||||
}
|
||||
|
||||
let addItemToCart;
|
||||
function App({cart}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const savedCartSize = cart.length;
|
||||
const [optimisticCartSize, setOptimisticCartSize] =
|
||||
useOptimistic(savedCartSize);
|
||||
|
||||
addItemToCart = item => {
|
||||
startTransition(async () => {
|
||||
setOptimisticCartSize(n => n + 1);
|
||||
await submitNewItem(item);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text text={'Pending: ' + isPending} />
|
||||
</div>
|
||||
<div>
|
||||
<Text text={'Items in cart: ' + optimisticCartSize} />
|
||||
</div>
|
||||
<ul>
|
||||
{cart.map(item => (
|
||||
<li key={item}>
|
||||
<Text text={'Item ' + item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => root.render(<App cart={serverCart} />));
|
||||
assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: false</div>
|
||||
<div>Items in cart: 1</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// The cart size is incremented even though B hasn't been added yet.
|
||||
await act(() => addItemToCart('B'));
|
||||
assertLog(['Pending: true', 'Items in cart: 2', 'Item A']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: true</div>
|
||||
<div>Items in cart: 2</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// While B is still pending, another item gets added to the cart
|
||||
// out-of-band.
|
||||
serverCart = [...serverCart, 'C'];
|
||||
// NOTE: This is a synchronous update only because we don't yet support
|
||||
// parallel transitions; all transitions are entangled together. Once we add
|
||||
// support for parallel transitions, we can update this test.
|
||||
ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
|
||||
assertLog([
|
||||
'Pending: true',
|
||||
// Note that the optimistic cart size is still correct, because the
|
||||
// pending update was rebased on top new value.
|
||||
'Items in cart: 3',
|
||||
'Item A',
|
||||
'Item C',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: true</div>
|
||||
<div>Items in cart: 3</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item C</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// Finish loading B. The optimistic state is reverted.
|
||||
await act(() => resolveText('Adding item B'));
|
||||
assertLog([
|
||||
'Pending: false',
|
||||
'Items in cart: 3',
|
||||
'Item A',
|
||||
'Item C',
|
||||
'Item B',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: false</div>
|
||||
<div>Items in cart: 3</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item C</li>
|
||||
<li>Item B</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useOptimistic accepts a custom reducer', async () => {
|
||||
let serverCart = ['A'];
|
||||
|
||||
async function submitNewItem(item) {
|
||||
await getText('Adding item ' + item);
|
||||
serverCart = [...serverCart, item];
|
||||
React.startTransition(() => {
|
||||
root.render(<App cart={serverCart} />);
|
||||
});
|
||||
}
|
||||
|
||||
let addItemToCart;
|
||||
function App({cart}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const savedCartSize = cart.length;
|
||||
const [optimisticCartSize, addToOptimisticCart] = useOptimistic(
|
||||
savedCartSize,
|
||||
(prevSize, newItem) => {
|
||||
Scheduler.log('Increment optimistic cart size for ' + newItem);
|
||||
return prevSize + 1;
|
||||
},
|
||||
);
|
||||
|
||||
addItemToCart = item => {
|
||||
startTransition(async () => {
|
||||
addToOptimisticCart(item);
|
||||
await submitNewItem(item);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text text={'Pending: ' + isPending} />
|
||||
</div>
|
||||
<div>
|
||||
<Text text={'Items in cart: ' + optimisticCartSize} />
|
||||
</div>
|
||||
<ul>
|
||||
{cart.map(item => (
|
||||
<li key={item}>
|
||||
<Text text={'Item ' + item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => root.render(<App cart={serverCart} />));
|
||||
assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: false</div>
|
||||
<div>Items in cart: 1</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// The cart size is incremented even though B hasn't been added yet.
|
||||
await act(() => addItemToCart('B'));
|
||||
assertLog([
|
||||
'Increment optimistic cart size for B',
|
||||
'Pending: true',
|
||||
'Items in cart: 2',
|
||||
'Item A',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: true</div>
|
||||
<div>Items in cart: 2</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// While B is still pending, another item gets added to the cart
|
||||
// out-of-band.
|
||||
serverCart = [...serverCart, 'C'];
|
||||
// NOTE: This is a synchronous update only because we don't yet support
|
||||
// parallel transitions; all transitions are entangled together. Once we add
|
||||
// support for parallel transitions, we can update this test.
|
||||
ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
|
||||
assertLog([
|
||||
'Increment optimistic cart size for B',
|
||||
'Pending: true',
|
||||
// Note that the optimistic cart size is still correct, because the
|
||||
// pending update was rebased on top new value.
|
||||
'Items in cart: 3',
|
||||
'Item A',
|
||||
'Item C',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: true</div>
|
||||
<div>Items in cart: 3</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item C</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
|
||||
// Finish loading B. The optimistic state is reverted.
|
||||
await act(() => resolveText('Adding item B'));
|
||||
assertLog([
|
||||
'Pending: false',
|
||||
'Items in cart: 3',
|
||||
'Item A',
|
||||
'Item C',
|
||||
'Item B',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Pending: false</div>
|
||||
<div>Items in cart: 3</div>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item C</li>
|
||||
<li>Item B</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useOptimistic rebases if the passthrough is updated during a render phase update', async () => {
|
||||
// This is kind of an esoteric case where it's hard to come up with a
|
||||
// realistic real-world scenario but it should still work.
|
||||
let increment;
|
||||
let setCount;
|
||||
function App() {
|
||||
const [isPending, startTransition] = useTransition(2);
|
||||
const [count, _setCount] = useState(0);
|
||||
setCount = _setCount;
|
||||
|
||||
const [optimisticCount, setOptimisticCount] = useOptimistic(
|
||||
count,
|
||||
prev => {
|
||||
Scheduler.log('Increment optimistic count');
|
||||
return prev + 1;
|
||||
},
|
||||
);
|
||||
|
||||
if (count === 1) {
|
||||
Scheduler.log('Render phase update count from 1 to 2');
|
||||
setCount(2);
|
||||
}
|
||||
|
||||
increment = () =>
|
||||
startTransition(async () => {
|
||||
setOptimisticCount(n => n + 1);
|
||||
await getText('Wait to increment');
|
||||
React.startTransition(() => setCount(n => n + 1));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text text={'Count: ' + count} />
|
||||
</div>
|
||||
{isPending ? (
|
||||
<div>
|
||||
<Text text={'Optimistic count: ' + optimisticCount} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => root.render(<App />));
|
||||
assertLog(['Count: 0']);
|
||||
expect(root).toMatchRenderedOutput(<div>Count: 0</div>);
|
||||
|
||||
await act(() => increment());
|
||||
assertLog([
|
||||
'Increment optimistic count',
|
||||
'Count: 0',
|
||||
'Optimistic count: 1',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Count: 0</div>
|
||||
<div>Optimistic count: 1</div>
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(() => setCount(1));
|
||||
assertLog([
|
||||
'Increment optimistic count',
|
||||
'Render phase update count from 1 to 2',
|
||||
// The optimistic update is rebased on top of the new passthrough value.
|
||||
'Increment optimistic count',
|
||||
'Count: 2',
|
||||
'Optimistic count: 3',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Count: 2</div>
|
||||
<div>Optimistic count: 3</div>
|
||||
</>,
|
||||
);
|
||||
|
||||
// Finish the action
|
||||
await act(() => resolveText('Wait to increment'));
|
||||
assertLog(['Count: 3']);
|
||||
expect(root).toMatchRenderedOutput(<div>Count: 3</div>);
|
||||
});
|
||||
|
||||
// @gate enableAsyncActions
|
||||
test('useOptimistic rebases if the passthrough is updated during a render phase update (initial mount)', async () => {
|
||||
// This is kind of an esoteric case where it's hard to come up with a
|
||||
// realistic real-world scenario but it should still work.
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
const [optimisticCount] = useOptimistic(count);
|
||||
|
||||
if (count === 0) {
|
||||
Scheduler.log('Render phase update count from 1 to 2');
|
||||
setCount(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text text={'Count: ' + count} />
|
||||
</div>
|
||||
<div>
|
||||
<Text text={'Optimistic count: ' + optimisticCount} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(() => root.render(<App />));
|
||||
assertLog([
|
||||
'Render phase update count from 1 to 2',
|
||||
'Count: 1',
|
||||
'Optimistic count: 1',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Count: 1</div>
|
||||
<div>Optimistic count: 1</div>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
let _doFlushWork;
|
||||
const shimHostConfigPath = 'react-reconciler/src/ReactFiberConfig';
|
||||
|
||||
jest.mock(shimHostConfigPath, () => {
|
||||
return jest.requireActual(
|
||||
'react-dom-bindings/src/client/ReactFiberConfigDOM.js',
|
||||
);
|
||||
});
|
||||
beforeAll(() => {
|
||||
_doFlushWork = require('../ReactFiberRootScheduler')._doFlushWork;
|
||||
});
|
||||
|
||||
test('does not hang', () => {
|
||||
const root = {
|
||||
tag: 1,
|
||||
pendingChildren: null,
|
||||
pingCache: {},
|
||||
finishedWork: null,
|
||||
timeoutHandle: -1,
|
||||
cancelPendingCommit: null,
|
||||
context: {},
|
||||
pendingContext: null,
|
||||
next: null,
|
||||
callbackNode: null,
|
||||
callbackPriority: 0,
|
||||
expirationTimes: [
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 278303.90000000596,
|
||||
278417.1999999881, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1,
|
||||
],
|
||||
pendingLanes: 6176,
|
||||
suspendedLanes: 0,
|
||||
pingedLanes: 0,
|
||||
expiredLanes: 0,
|
||||
mutableReadLanes: 0,
|
||||
finishedLanes: 0,
|
||||
errorRecoveryDisabledLanes: 0,
|
||||
entangledLanes: 6144,
|
||||
entanglements: [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6144, 6144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
],
|
||||
hiddenUpdates: [],
|
||||
identifierPrefix: '',
|
||||
pooledCache: null,
|
||||
pooledCacheLanes: 0,
|
||||
mutableSourceEagerHydrationData: null,
|
||||
hydrationCallbacks: {
|
||||
unstable_concurrentUpdatesByDefault: true,
|
||||
unstable_strictMode: true,
|
||||
},
|
||||
incompleteTransitions: {},
|
||||
effectDuration: 0,
|
||||
passiveEffectDuration: 0,
|
||||
memoizedUpdaters: {},
|
||||
pendingUpdatersLaneMap: [],
|
||||
_debugRootType: 'hydrateRoot()',
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
_doFlushWork(root, root, 2, false);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
|
@ -1811,6 +1811,102 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// @gate enableLegacyCache
|
||||
it('throttles content from appearing if a fallback was filled in recently', async () => {
|
||||
function Foo() {
|
||||
Scheduler.log('Foo');
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<Text text="Loading A..." />}>
|
||||
<AsyncText text="A" />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Text text="Loading B..." />}>
|
||||
<AsyncText text="B" />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Foo />);
|
||||
// Start rendering
|
||||
await waitForAll([
|
||||
'Foo',
|
||||
'Suspend! [A]',
|
||||
'Loading A...',
|
||||
'Suspend! [B]',
|
||||
'Loading B...',
|
||||
]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="Loading A..." />
|
||||
<span prop="Loading B..." />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Resolve only A. B will still be loading.
|
||||
await act(async () => {
|
||||
await resolveText('A');
|
||||
|
||||
// If we didn't advance the time here, A would not commit; it would
|
||||
// be throttled because the fallback would have appeared too recently.
|
||||
Scheduler.unstable_advanceTime(10000);
|
||||
jest.advanceTimersByTime(10000);
|
||||
await waitForPaint(['A']);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="A" />
|
||||
<span prop="Loading B..." />
|
||||
</>,
|
||||
);
|
||||
});
|
||||
|
||||
// Advance by a small amount of time. For testing purposes, this is meant
|
||||
// to be just under the throttling interval. It's a heurstic, though, so
|
||||
// if we adjust the heuristic we might have to update this test, too.
|
||||
Scheduler.unstable_advanceTime(200);
|
||||
jest.advanceTimersByTime(200);
|
||||
|
||||
// Now resolve B.
|
||||
await act(async () => {
|
||||
await resolveText('B');
|
||||
await waitForPaint(['B']);
|
||||
|
||||
if (gate(flags => flags.alwaysThrottleRetries)) {
|
||||
// B should not commit yet. Even though it's been a long time since its
|
||||
// fallback was shown, it hasn't been long since A appeared. So B's
|
||||
// appearance is throttled to reduce jank.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="A" />
|
||||
<span prop="Loading B..." />
|
||||
</>,
|
||||
);
|
||||
|
||||
// Advance time a little bit more. Now it commits because enough time
|
||||
// has passed.
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
jest.advanceTimersByTime(100);
|
||||
await waitForAll([]);
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="A" />
|
||||
<span prop="B" />
|
||||
</>,
|
||||
);
|
||||
} else {
|
||||
// Old behavior, gated until this rolls out at Meta:
|
||||
//
|
||||
// B appears immediately, without being throttled.
|
||||
expect(ReactNoop).toMatchRenderedOutput(
|
||||
<>
|
||||
<span prop="A" />
|
||||
<span prop="B" />
|
||||
</>,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: flip to "warns" when this is implemented again.
|
||||
// @gate enableLegacyCache
|
||||
it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
|
||||
|
|
|
|||
|
|
@ -149,12 +149,6 @@ export const getFirstHydratableChildWithinContainer =
|
|||
$$$config.getFirstHydratableChildWithinContainer;
|
||||
export const getFirstHydratableChildWithinSuspenseInstance =
|
||||
$$$config.getFirstHydratableChildWithinSuspenseInstance;
|
||||
export const shouldSkipHydratableForInstance =
|
||||
$$$config.shouldSkipHydratableForInstance;
|
||||
export const shouldSkipHydratableForTextInstance =
|
||||
$$$config.shouldSkipHydratableForTextInstance;
|
||||
export const shouldSkipHydratableForSuspenseInstance =
|
||||
$$$config.shouldSkipHydratableForSuspenseInstance;
|
||||
export const canHydrateInstance = $$$config.canHydrateInstance;
|
||||
export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
|
||||
export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;
|
||||
|
|
|
|||
|
|
@ -1,10 +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-native-renderer/src/ReactFiberConfigFabric';
|
||||
|
|
@ -657,7 +657,6 @@ export function createSignatureFunctionForTransform(): <T>(
|
|||
// in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))).
|
||||
if (!savedType) {
|
||||
// We're in the innermost call, so this is the actual type.
|
||||
// $FlowFixMe[escaped-generic] discovered when updating Flow
|
||||
savedType = type;
|
||||
hasCustomHooks = typeof getCustomHooks === 'function';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
{
|
||||
"name": "react-server-native-relay",
|
||||
"name": "react-server-dom-fb",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-server-native-relay"
|
||||
"directory": "packages/react-server-dom-fb"
|
||||
},
|
||||
"dependencies": {
|
||||
"scheduler": "^0.11.0"
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0"
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"name": "react-server-dom-relay",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-server-dom-relay"
|
||||
},
|
||||
"dependencies": {
|
||||
"scheduler": "^0.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +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 './src/ReactFlightDOMRelayServer';
|
||||
|
|
@ -1,97 +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 {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {JSResourceReference} from 'JSResourceReference';
|
||||
|
||||
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
|
||||
|
||||
export type ClientReference<T> = JSResourceReference<T>;
|
||||
|
||||
import {
|
||||
parseModelString,
|
||||
parseModelTuple,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
export {
|
||||
preloadModule,
|
||||
requireModule,
|
||||
} from 'ReactFlightDOMRelayClientIntegration';
|
||||
|
||||
import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
export type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
|
||||
|
||||
export type SSRManifest = null;
|
||||
export type ServerManifest = null;
|
||||
export type ServerReferenceId = string;
|
||||
|
||||
export type UninitializedModel = JSONValue;
|
||||
|
||||
export type Response = ResponseBase;
|
||||
|
||||
export function resolveClientReference<T>(
|
||||
bundlerConfig: SSRManifest,
|
||||
metadata: ClientReferenceMetadata,
|
||||
): ClientReference<T> {
|
||||
return resolveClientReferenceImpl(metadata);
|
||||
}
|
||||
|
||||
export function resolveServerReference<T>(
|
||||
bundlerConfig: ServerManifest,
|
||||
id: ServerReferenceId,
|
||||
): ClientReference<T> {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
function parseModelRecursively(
|
||||
response: Response,
|
||||
parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
||||
key: string,
|
||||
value: JSONValue,
|
||||
): $FlowFixMe {
|
||||
if (typeof value === 'string') {
|
||||
return parseModelString(response, parentObj, key, value);
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (isArray(value)) {
|
||||
const parsedValue: Array<$FlowFixMe> = [];
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
(parsedValue: any)[i] = parseModelRecursively(
|
||||
response,
|
||||
value,
|
||||
'' + i,
|
||||
value[i],
|
||||
);
|
||||
}
|
||||
return parseModelTuple(response, parsedValue);
|
||||
} else {
|
||||
const parsedValue = {};
|
||||
for (const innerKey in value) {
|
||||
(parsedValue: any)[innerKey] = parseModelRecursively(
|
||||
response,
|
||||
value,
|
||||
innerKey,
|
||||
value[innerKey],
|
||||
);
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const dummy = {};
|
||||
|
||||
export function parseModel<T>(response: Response, json: UninitializedModel): T {
|
||||
return (parseModelRecursively(response, dummy, '', json): any);
|
||||
}
|
||||
|
|
@ -1,61 +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 {RowEncoding} from './ReactFlightDOMRelayProtocol';
|
||||
|
||||
import type {Response} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import {
|
||||
createResponse,
|
||||
resolveModel,
|
||||
resolveModule,
|
||||
resolveErrorDev,
|
||||
resolveErrorProd,
|
||||
resolveHint,
|
||||
close,
|
||||
getRoot,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
export {createResponse, close, getRoot};
|
||||
|
||||
export function resolveRow(response: Response, chunk: RowEncoding): void {
|
||||
if (chunk[0] === 'O') {
|
||||
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
||||
resolveModel(response, chunk[1], chunk[2]);
|
||||
} else if (chunk[0] === 'I') {
|
||||
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
||||
resolveModule(response, chunk[1], chunk[2]);
|
||||
} else if (chunk[0] === 'H') {
|
||||
// $FlowFixMe[incompatible-call] unable to refine on array indices
|
||||
resolveHint(response, chunk[1], chunk[2]);
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
resolveErrorDev(
|
||||
response,
|
||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||
chunk[1],
|
||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||
// $FlowFixMe[prop-missing]
|
||||
// $FlowFixMe[incompatible-use]
|
||||
chunk[2].digest,
|
||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||
// $FlowFixMe[incompatible-use]
|
||||
chunk[2].message || '',
|
||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||
// $FlowFixMe[incompatible-use]
|
||||
chunk[2].stack || '',
|
||||
);
|
||||
} else {
|
||||
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
|
||||
// $FlowFixMe[prop-missing]
|
||||
// $FlowFixMe[incompatible-use]
|
||||
resolveErrorProd(response, chunk[1], chunk[2].digest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +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 {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
|
||||
|
||||
export type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| {+[key: string]: JSONValue}
|
||||
| $ReadOnlyArray<JSONValue>;
|
||||
|
||||
export type RowEncoding =
|
||||
| ['O', number, JSONValue]
|
||||
| ['I', number, ClientReferenceMetadata]
|
||||
| ['H', string, HintModel]
|
||||
| ['P', number, string]
|
||||
| ['S', number, string]
|
||||
| [
|
||||
'E',
|
||||
number,
|
||||
{
|
||||
digest: string,
|
||||
message?: string,
|
||||
stack?: string,
|
||||
...
|
||||
},
|
||||
];
|
||||
|
|
@ -1,44 +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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
ClientManifest,
|
||||
Destination,
|
||||
} from './ReactFlightServerConfigDOMRelay';
|
||||
|
||||
import {
|
||||
createRequest,
|
||||
startWork,
|
||||
startFlowing,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
type Options = {
|
||||
onError?: (error: mixed) => void,
|
||||
identifierPrefix?: string,
|
||||
};
|
||||
|
||||
function render(
|
||||
model: ReactClientValue,
|
||||
destination: Destination,
|
||||
config: ClientManifest,
|
||||
options?: Options,
|
||||
): void {
|
||||
const request = createRequest(
|
||||
model,
|
||||
config,
|
||||
options ? options.onError : undefined,
|
||||
undefined, // not currently set up to supply context overrides
|
||||
options ? options.identifierPrefix : undefined,
|
||||
);
|
||||
startWork(request);
|
||||
startFlowing(request, destination);
|
||||
}
|
||||
|
||||
export {render};
|
||||
|
|
@ -1,236 +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 {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||
import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol';
|
||||
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import type {JSResourceReference} from 'JSResourceReference';
|
||||
import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
export type ClientReference<T> = JSResourceReference<T>;
|
||||
export type ServerReference<T> = T;
|
||||
export type ServerReferenceId = {};
|
||||
|
||||
import type {
|
||||
Destination,
|
||||
BundlerConfig as ClientManifest,
|
||||
ClientReferenceMetadata,
|
||||
} from 'ReactFlightDOMRelayServerIntegration';
|
||||
|
||||
import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
emitRow,
|
||||
resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl,
|
||||
close,
|
||||
} from 'ReactFlightDOMRelayServerIntegration';
|
||||
|
||||
export type {
|
||||
Destination,
|
||||
BundlerConfig as ClientManifest,
|
||||
ClientReferenceMetadata,
|
||||
} from 'ReactFlightDOMRelayServerIntegration';
|
||||
|
||||
export function isClientReference(reference: Object): boolean {
|
||||
return reference instanceof JSResourceReferenceImpl;
|
||||
}
|
||||
|
||||
export function isServerReference(reference: Object): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export type ClientReferenceKey = ClientReference<any>;
|
||||
|
||||
export function getClientReferenceKey(
|
||||
reference: ClientReference<any>,
|
||||
): ClientReferenceKey {
|
||||
// We use the reference object itself as the key because we assume the
|
||||
// object will be cached by the bundler runtime.
|
||||
return reference;
|
||||
}
|
||||
|
||||
export function resolveClientReferenceMetadata<T>(
|
||||
config: ClientManifest,
|
||||
resource: ClientReference<T>,
|
||||
): ClientReferenceMetadata {
|
||||
return resolveClientReferenceMetadataImpl(config, resource);
|
||||
}
|
||||
|
||||
export function getServerReferenceId<T>(
|
||||
config: ClientManifest,
|
||||
resource: ServerReference<T>,
|
||||
): ServerReferenceId {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
export function getServerReferenceBoundArguments<T>(
|
||||
config: ClientManifest,
|
||||
resource: ServerReference<T>,
|
||||
): Array<ReactClientValue> {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
export type Chunk = RowEncoding;
|
||||
|
||||
export function processErrorChunkProd(
|
||||
request: Request,
|
||||
id: number,
|
||||
digest: string,
|
||||
): Chunk {
|
||||
if (__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'E',
|
||||
id,
|
||||
{
|
||||
digest,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function processErrorChunkDev(
|
||||
request: Request,
|
||||
id: number,
|
||||
digest: string,
|
||||
message: string,
|
||||
stack: string,
|
||||
): Chunk {
|
||||
if (!__DEV__) {
|
||||
// These errors should never make it into a build so we don't need to encode them in codes.json
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'E',
|
||||
id,
|
||||
{
|
||||
digest,
|
||||
message,
|
||||
stack,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function convertModelToJSON(
|
||||
request: Request,
|
||||
parent: {+[key: string]: ReactClientValue} | $ReadOnlyArray<ReactClientValue>,
|
||||
key: string,
|
||||
model: ReactClientValue,
|
||||
): JSONValue {
|
||||
const json = resolveModelToJSON(request, parent, key, model);
|
||||
if (typeof json === 'object' && json !== null) {
|
||||
if (isArray(json)) {
|
||||
const jsonArray: Array<JSONValue> = [];
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
jsonArray[i] = convertModelToJSON(request, json, '' + i, json[i]);
|
||||
}
|
||||
return jsonArray;
|
||||
} else {
|
||||
const jsonObj: {[key: string]: JSONValue} = {};
|
||||
for (const nextKey in json) {
|
||||
if (hasOwnProperty.call(json, nextKey)) {
|
||||
jsonObj[nextKey] = convertModelToJSON(
|
||||
request,
|
||||
json,
|
||||
nextKey,
|
||||
json[nextKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
return jsonObj;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export function processModelChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
model: ReactClientValue,
|
||||
): Chunk {
|
||||
const json = convertModelToJSON(request, {}, '', model);
|
||||
return ['O', id, json];
|
||||
}
|
||||
|
||||
export function processReferenceChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
reference: string,
|
||||
): Chunk {
|
||||
return ['O', id, reference];
|
||||
}
|
||||
|
||||
export function processImportChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
clientReferenceMetadata: ClientReferenceMetadata,
|
||||
): Chunk {
|
||||
// The clientReferenceMetadata is already a JSON serializable value.
|
||||
return ['I', id, clientReferenceMetadata];
|
||||
}
|
||||
|
||||
export function processHintChunk(
|
||||
request: Request,
|
||||
id: number,
|
||||
code: string,
|
||||
model: HintModel,
|
||||
): Chunk {
|
||||
// The hint is already a JSON serializable value.
|
||||
return ['H', code, model];
|
||||
}
|
||||
|
||||
export function scheduleWork(callback: () => void) {
|
||||
callback();
|
||||
}
|
||||
|
||||
export function flushBuffered(destination: Destination) {}
|
||||
|
||||
export const supportsRequestStorage = false;
|
||||
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
|
||||
|
||||
export function beginWriting(destination: Destination) {}
|
||||
|
||||
export function writeChunk(destination: Destination, chunk: Chunk): void {
|
||||
// $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
|
||||
emitRow(destination, chunk);
|
||||
}
|
||||
|
||||
export function writeChunkAndReturn(
|
||||
destination: Destination,
|
||||
chunk: Chunk,
|
||||
): boolean {
|
||||
// $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
|
||||
emitRow(destination, chunk);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function completeWriting(destination: Destination) {}
|
||||
|
||||
export {close};
|
||||
|
||||
export function closeWithError(destination: Destination, error: mixed): void {
|
||||
close(destination);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue