Compare commits

..

26 Commits

Author SHA1 Message Date
Jorge Cabiedes
555f844195 Formatting 2025-07-03 13:32:15 -07:00
Jorge Cabiedes
c94e8b4461 Implement all tools to react-tools-cli 2025-07-03 13:06:21 -07:00
Jorge Cabiedes
6b04874535 Add react-tools-cli commands for each tool on the mcp 2025-07-01 11:31:30 -07:00
Jorge Cabiedes Acosta
1ba1485a65 Create react-tools-cli package and import react-mcp-server 2025-07-01 09:01:36 -07:00
Jorge Cabiedes Acosta
2c1e4e4513 Reformat code to modularize tools 2025-06-24 22:56:21 -07:00
Sebastian Markbåge
e67b4fe22e [Flight] Emit Partial Debug Info if we have any at the point of aborting a render (#33632)
When we abort a render we don't really have much information about the
task that was aborted. Because before a Promise resolves there's no
indication about would have resolved it. In particular we don't know
which I/O would've ultimately called resolve().

However, we can at least emit any information we do have at the point
where we emit it. At the least the stack of the top most Promise.

Currently we synchronously flush at the end of an `abort()` but we
should ideally schedule the flush in a macrotask and emit this debug
information right before that. That way we would give an opportunity for
any `cacheSignal()` abort to trigger rejections all the way up and those
rejections informs the awaited stack.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-24 16:36:21 -04:00
Sebastian Markbåge
4a523489b7 Get Server Component Function Location for Parent Stacks using Child's Owner Stack (#33629)
This is using the same trick as #30798 but for runtime code too. It's
essential zero cost.

This lets us include a source location for parent stacks of Server
Components when it has an owned child's location. Either from JSX or
I/O.

Ironically, a Component that throws an error will likely itself not get
the stack because it won't have any JSX rendered yet.
2025-06-24 16:35:28 -04:00
Joseph Savona
94cf60bede [compiler] New inference repros/fixes (#33584)
Substantially improves the last major known issue with the new inference
model's implementation: inferring effects of function expressions. I
knowingly used a really simple (dumb) approach in
InferFunctionExpressionAliasingEffects but it worked surprisingly well
on a ton of code. However, investigating during the sync I saw that we
the algorithm was literally running out of memory, or crashing from
arrays that exceeded the maximum capacity. We were accumluating data
flow in a way that could lead to lists of data flow captures compounding
on themselves and growing very large very quickly. Plus, we were
incorrectly recording some data flow, leading to cases where we reported
false positive "can't mutate frozen value" for example.

So I went back to the drawing board. InferMutationAliasingRanges already
builds up a data flow graph which it uses to figure out what values
would be affected by mutations of other values, and update mutable
ranges. Well, the key question that we really want to answer for
inferring a function expression's aliasing effects is which values
alias/capture where. Per the docs I wrote up, we only have to record
such aliasing _if they are observable via mutations_. So, lightbulb:
simulate mutations of the params, free variables, and return of the
function expression and see which params/free-vars would be affected!
That's what we do now, giving us precise information about which such
values alias/capture where. When the "into" is a param/context-var we
use Capture, iwhen the destination is the return we use Alias to be
conservative.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33584).
* #33626
* #33625
* #33624
* __->__ #33584
2025-06-24 10:01:58 -07:00
Sebastian Markbåge
bbc13fa17b [Flight] Add Debug Channel option for stateful connection to the backend in DEV (#33627)
This adds plumbing for opening a stream from the Flight Client to the
Flight Server so it can ask for more data on-demand. In this mode, the
Flight Server keeps the connection open as long as the client is still
alive and there's more objects to load. It retains any depth limited
objects so that they can be asked for later. In this first PR it just
releases the object when it's discovered on the server and doesn't
actually lazy load it yet. That's coming in a follow up.

This strategy is built on the model that each request has its own
channel for this. Instead of some global registry. That ensures that
referential identity is preserved within a Request and the Request can
refer to previously written objects by reference.

The fixture implements a WebSocket per request but it doesn't have to be
done that way. It can be multiplexed through an existing WebSocket for
example. The current protocol is just a Readable(Stream) on the server
and WritableStream on the client. It could even be sent through a HTTP
request body if browsers implemented full duplex (which they don't).

This PR only implements the direction of messages from Client to Server.
However, I also plan on adding Debug Channel in the other direction to
allow debug info (optionally) be sent from Server to Client through this
channel instead of through the main RSC request. So the `debugChannel`
option will be able to take writable or readable or both.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-24 11:16:09 -04:00
Ricky
12eaef7ef5 [refactor] remove unused fiberstack functions (#33623) 2025-06-23 20:07:04 -04:00
Sebastian Markbåge
c80c69fa96 [Flight] Remove back pointers to the Response from the Chunks (#33620)
This frees some memory that will be even more important in a follow up.

Currently, all `ReactPromise` instances hold onto their original
`Response`. The `Response` holds onto all objects that were in that
response since they're needed in case the parsed content ends up
referring to an existing object. If everything you retain are plain
objects then that's fine and the `Response` gets GC:ed, but if you're
retaining a `Promise` itself then it holds onto the whole `Response`.

The only thing that needs this reference at all is a
`ResolvedModelChunk` since it will lazily initialize e.g. by calling
`.then` on itself and so we need to know where to find any sibling
chunks it may refer to. However, we can just store the `Response` on the
`reason` field for this particular state.

That way when all lazy values are touched and initialized the `Response`
is freed. We also free up some memory by getting rid of the extra field.
2025-06-23 18:37:52 -04:00
Jan Kassens
aab72cb1cb rename ReactFiberContext to ReactFiberLegacyContext (#33622)
It wasn't immediately obvious to me, that all the exports here are
related to legacy context, so renaming for clarity.

Modern context lives in `ReactFiberNewContext` which we could probably
also raname in a separate step to just Context.
2025-06-23 17:21:18 -04:00
Sebastian "Sebbie" Silbermann
fa3feba672 Fix prelease workflows for dry: false (#33582)
## Summary

Follow-up to https://github.com/facebook/react/pull/33525

Fixes `Unsupported tag: "false"`
(https://github.com/facebook/react/actions/runs/15773778995/job/44463562733#step:13:12)
which also affects nightly releases.

## How did you test this change?

- [x] Run successful, manual prerelease from this branch:
https://github.com/facebook/react/actions/runs/15774083406
2025-06-23 11:47:07 -04:00
Sebastian Markbåge
2a911f27dd [Flight] Send the awaited Promise to the client as additional debug information (#33592)
Stacked on #33588, #33589 and #33590.

This lets us automatically show the resolved value in the UI.

<img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM"
src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4"
/>

We can also show rejected I/O that may or may not have been handled with
the error message.

<img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM"
src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1"
/>

To get this working we need to keep the Promise around for longer so
that we can access it once we want to emit an async sequence. I do this
by storing the WeakRefs but to ensure that the Promise doesn't get
garbage collected, I keep a WeakMap of Promise to the Promise that it
depended on. This lets the VM still clean up any Promise chains that
have leaves that are cleaned up. So this makes Promises live until the
last Promise downstream is done. At that point we can go back up the
chain to read the values out of them.

Additionally, to get the best possible value we don't want to get a
Promise that's used by internals of a third-party function. We want the
value that the first party gets to observe. To do this I had to change
the logic for which "await" to use, to be the one that is the first
await that happened in user space. It's not enough that the await has
any first party at all on the stack - it has to be the very first frame.
This is a little sketchy because it relies on the `.then()` call or
`await` call not having any third party wrappers. But it gives the best
object since it hides all the internals. For example when you call
`fetch()` we now log that actual `Response` object.
2025-06-23 10:12:45 -04:00
Sebastian Markbåge
18ee505e77 [Flight] Support classes in renderDebugModel (#33590)
This adds better support for serializing class instances as Debug
values.

It adds a new marker on the object `{ "": "$P...", ... }` which
indicates which constructor's prototype to use for this object's
prototype. It doesn't encode arbitrary prototypes and it doesn't encode
any of the properties on the prototype. It might get some of the
properties from the prototype by virtue of `toString` on a `class`
constructor will include the whole class's body.

This will ensure that the instance gets the right name in logs.

Additionally, this now also invokes getters if they're enumerable on the
prototype. This lets us reify values that can only be read from native
classes.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-06-22 18:00:08 -04:00
Sebastian Markbåge
1d1b26c701 [Flight] Serialize already resolved Promises as debug models (#33588)
We already support serializing the values of instrumented Promises as
debug values such as in console logs. However, we don't support plain
native promises.

This waits a microtask to see if we can read the value within a
microtask and if so emit it. This is so that we can still close the
connection.

Otherwise, we emit a "halted" row into its row id which replaces the old
"Infinite Promise" reference.

We could potentially wait until the end of the render before cancelling
so that if it resolves before we exit we can still include its value but
that would require a bit more work. Ideally we'd have a way to get these
lazily later anyway.
2025-06-22 17:51:31 -04:00
Sebastian Markbåge
fe3f0ec037 [Flight] Don't use object property initializer for async iterable (#33591)
It turns out this was being compiled to a `_defineProperty` helper by
Babel or Closure. We're supposed to have it error the build when we use
features like this that might get compiled.

We should stick to simple ES5 features.
2025-06-22 10:40:56 -04:00
Sebastian Markbåge
d70ee32b88 [Flight] Eagerly parse stack traces in DebugNode (#33589)
There's a memory leak in DebugNode where the `Error` objects that we
instantiate retains their callstacks which can have Promises on them. In
fact, it's very likely since the current callsite has the "resource" on
it which is the Promise itself. If those Promises are retained then
their `destroy` async hook is never fired which doesn't clean up our map
which can contains the `Error` object. Creating a cycle that can't be
cleaned up.

This fix is just eagerly reifying and parsing the stacks.

I totally expect this to be crazy slow since there's so many Promises
that we end up not needing to visit otherwise. We'll need to optimize it
somehow. Perhaps by being smarter about which ones we might need stacks
for. However, at least it doesn't leak indefinitely.
2025-06-22 10:40:33 -04:00
Sebastian Markbåge
6c7b1a1d98 Rename serializeConsoleMap/Set to serializeDebugMap/Set (#33587)
Follow up to #33583. I forgot to rename these too.
2025-06-21 10:36:07 -04:00
Sebastian Markbåge
ed077194b5 [Flight] Dedupe objects serialized as Debug Models in a separate set (#33583)
Stacked on #33539.

Stores dedupes of `renderConsoleValue` in a separate set. This allows us
to dedupe objects safely since we can't write objects using this
algorithm if they might also be referenced by the "real" serialization.

Also renamed it to `renderDebugModel` since it's not just for console
anymore.
2025-06-20 13:36:39 -04:00
Devon Govett
643257ca52 [Flight] Serialize functions by reference (#33539)
On pages that have a high number of server components (e.g. common when
doing syntax highlighting), the debug outlining can produce extremely
large RSC payloads. For example a documentation page I was working on
had a 13.8 MB payload. I noticed that a majority of this was the source
code for the same function components repeated over and over again (over
4000 times) within `$E()` eval commands.

This PR deduplicates the same functions by serializing by reference,
similar to what is already done for objects. Doing this reduced the
payload size of my page from 13.8 MB to 4.6 MB, and resulted in only 31
evals instead of over 4000. As a result it reduced development page load
and hydration time from 4 seconds to 1.5 seconds. It also means the
deserialized functions will have reference equality just as they did on
the server.
2025-06-20 13:36:07 -04:00
Sebastian "Sebbie" Silbermann
06e89951be [Fizz] Ignore error if content node is gone before reveal (#33531) 2025-06-20 14:21:57 +02:00
Sebastian Markbåge
79d9aed7ed [Fizz] Clean up the replay nodes if we're already rendered past an element (#33581) 2025-06-20 09:26:26 +02:00
Sebastian "Sebbie" Silbermann
c8822e926b Make it clearer what runtime release failed (#33579) 2025-06-20 09:11:27 +02:00
Sebastian "Sebbie" Silbermann
a947eba4f2 Fix CI (#33578) 2025-06-19 23:40:59 +02:00
Ruslan Lesiutin
374dfe8edf build: make enableComponentPerformanceTrack dynamic for native-fb (#33560)
## Summary

Make this flag dynamic, so it can be controlled internally.

## How did you test this change?

Build, observe that `console.timeStamp` is only present in FB artifacts
and `enableComponentPerformanceTrack` is referenced.
2025-06-19 09:47:23 +01:00
66 changed files with 3681 additions and 941 deletions

View File

@@ -301,10 +301,12 @@ jobs:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test

View File

@@ -85,7 +85,7 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -94,19 +94,19 @@ jobs:
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}

View File

@@ -110,7 +110,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -119,7 +119,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry'}}
${{ inputs.dry && '--dry' || '' }}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:

View File

@@ -8,20 +8,14 @@
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod';
import {compile, type PrintedCompilerPipelineValue} from './compiler';
import {
CompilerPipelineValue,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
PluginOptions,
SourceLocation,
} from 'babel-plugin-react-compiler/src';
import * as cheerio from 'cheerio';
import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
import {parseReactComponentTree} from './tools/componentTree';
import {
runtimePerfTool,
componentTreeTool,
compileTool,
devDocsTool,
} from './tools';
export type {PassNameType} from './tools';
function calculateMean(values: number[]): string {
return values.length > 0
@@ -41,38 +35,27 @@ server.tool(
query: z.string(),
},
async ({query}) => {
try {
const pages = await queryAlgolia(query);
if (pages.length === 0) {
const result = await devDocsTool(query);
switch (result.kind) {
case 'success': {
return {
content: [{type: 'text' as const, text: `No results`}],
isError: false,
content: result.content.map(text => {
return {
type: 'text' as const,
text: text,
};
}),
};
}
const content = pages.map(html => {
const $ = cheerio.load(html);
// react.dev should always have at least one <article> with the main content
const article = $('article').html();
if (article != null) {
return {
type: 'text' as const,
text: convert(article),
};
} else {
return {
type: 'text' as const,
// Fallback to converting the whole page to text.
text: convert($.html()),
};
}
});
return {
content,
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
case 'error':
return {
isError: true,
content: [{type: 'text' as const, text: result.text}],
};
default:
assertExhaustive(result, `Unhandled result ${JSON.stringify(result)}`);
}
},
);
@@ -93,199 +76,47 @@ server.tool(
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
},
async ({text, passName}) => {
const pipelinePasses = new Map<
string,
Array<PrintedCompilerPipelineValue>
>();
const recordPass: (
result: PrintedCompilerPipelineValue,
) => void = result => {
const entry = pipelinePasses.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
pipelinePasses.set(result.name, [result]);
}
};
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
recordPass({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
recordPass({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
recordPass({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
assertExhaustive(result, `Unhandled result ${result}`);
}
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,
logEvent: (_filename, event): void => {
if (event.kind === 'CompileError') {
const detail = event.detail;
const loc =
detail.loc == null || typeof detail.loc == 'symbol'
? event.fnLoc
: detail.loc;
errors.push({
message: detail.reason,
loc,
});
}
},
},
};
try {
const result = await compile({
text,
file: 'anonymous.tsx',
options: compilerOptions,
});
if (result.code == null) {
const results = await compileTool(text, passName);
switch (results.kind) {
case 'success': {
return {
isError: true,
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
};
}
const requestedPasses: Array<{type: 'text'; text: string}> = [];
if (passName != null) {
switch (passName) {
case 'All': {
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
if (hir !== undefined) {
for (const pipelineValue of hir) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
if (reactiveFunc !== undefined) {
for (const pipelineValue of reactiveFunc) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
break;
}
case 'HIR': {
// Last pass before HIR -> ReactiveFunction
const requestedPass = pipelinePasses.get(
'PropagateScopeDependenciesHIR',
);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case 'ReactiveFunction': {
// Last pass
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case '@DEBUG': {
for (const [, pipelinePass] of pipelinePasses) {
for (const pass of pipelinePass) {
requestedPasses.push({
type: 'text' as const,
text: `${pass.name}\n\n${pass.value}`,
});
}
}
break;
}
default: {
assertExhaustive(
passName,
`Unhandled passName option: ${passName}`,
);
}
}
const requestedPass = pipelinePasses.get(passName);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
if (pipelineValue.name === passName) {
requestedPasses.push({
type: 'text' as const,
text: pipelineValue.value,
});
}
}
}
}
if (errors.length > 0) {
return {
content: errors.map(err => {
isError: false,
content: results.content.map(text => {
return {
type: 'text' as const,
text:
err.loc === null || typeof err.loc === 'symbol'
? `React Compiler bailed out:\n\n${err.message}`
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
text,
};
}),
};
}
return {
content: [
{type: 'text' as const, text: result.code},
...requestedPasses,
],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
};
case 'bailout': {
return {
isError: true,
content: results.content.map(text => {
return {
type: 'text' as const,
text,
};
}),
};
}
case 'error':
case 'compile-error':
return {
isError: true,
content: [
{
type: 'text' as const,
text: results.text,
},
],
};
default:
assertExhaustive(
results,
`Unhandled result ${JSON.stringify(results)}`,
);
}
},
);
@@ -328,7 +159,7 @@ server.tool(
},
async ({text, iterations}) => {
try {
const results = await measurePerformance(text, iterations);
const results = await runtimePerfTool(text, iterations);
const formattedResults = `
# React Component Performance Results
@@ -387,7 +218,7 @@ server.tool(
},
async ({url}) => {
try {
const componentTree = await parseReactComponentTree(url);
const componentTree = await componentTreeTool(url);
return {
content: [
@@ -491,7 +322,17 @@ async function main() {
console.error('React Compiler MCP Server running on stdio');
}
main().catch(error => {
console.error('Fatal error in main():', error);
process.exit(1);
});
if (require.main !== module) {
main().catch(error => {
console.error('Fatal error in main():', error);
process.exit(1);
});
}
export {
compileTool,
componentTreeTool,
devDocsTool,
runtimePerfTool,
assertExhaustive,
};

View File

@@ -0,0 +1,203 @@
import {compile, type PrintedCompilerPipelineValue} from '../compiler';
import {
CompilerPipelineValue,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
PluginOptions,
SourceLocation,
} from 'babel-plugin-react-compiler/src';
import assertExhaustive from '../utils/assertExhaustive';
export type PassNameType =
| 'HIR'
| 'ReactiveFunction'
| 'All'
| '@DEBUG'
| undefined;
type CompilerToolOutput =
| {
kind: 'success';
content: Array<string>;
}
| {
kind: 'bailout';
content: Array<string>;
}
| {
kind: 'compile-error';
text: string;
}
| {
kind: 'error';
text: string;
};
export async function compileTool(
text: string,
passName: PassNameType,
): Promise<CompilerToolOutput> {
const pipelinePasses = new Map<string, Array<PrintedCompilerPipelineValue>>();
const recordPass: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = pipelinePasses.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
pipelinePasses.set(result.name, [result]);
}
};
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
recordPass({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
recordPass({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
recordPass({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
assertExhaustive(result, `Unhandled result ${result}`);
}
}
};
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
const compilerOptions: Partial<PluginOptions> = {
panicThreshold: 'none',
logger: {
debugLogIRs: logIR,
logEvent: (_filename, event): void => {
if (event.kind === 'CompileError') {
const detail = event.detail;
const loc =
detail.loc == null || typeof detail.loc == 'symbol'
? event.fnLoc
: detail.loc;
errors.push({
message: detail.reason,
loc,
});
}
},
},
};
try {
const result = await compile({
text,
file: 'anonymous.tsx',
options: compilerOptions,
});
if (result.code == null) {
return {
kind: 'compile-error',
text: 'Error: Could not compile',
};
}
const requestedPasses: Array<string> = [];
if (passName != null) {
switch (passName) {
case 'All': {
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
if (hir !== undefined) {
for (const pipelineValue of hir) {
requestedPasses.push(pipelineValue.value);
}
}
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
if (reactiveFunc !== undefined) {
for (const pipelineValue of reactiveFunc) {
requestedPasses.push(pipelineValue.value);
}
}
break;
}
case 'HIR': {
// Last pass before HIR -> ReactiveFunction
const requestedPass = pipelinePasses.get(
'PropagateScopeDependenciesHIR',
);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push(pipelineValue.value);
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case 'ReactiveFunction': {
// Last pass
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
requestedPasses.push(pipelineValue.value);
}
} else {
console.error(`Could not find requested pass ${passName}`);
}
break;
}
case '@DEBUG': {
for (const [, pipelinePass] of pipelinePasses) {
for (const pass of pipelinePass) {
requestedPasses.push(`${pass.name}\n\n${pass.value}`);
}
}
break;
}
default: {
assertExhaustive(passName, `Unhandled passName option: ${passName}`);
}
}
const requestedPass = pipelinePasses.get(passName);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
if (pipelineValue.name === passName) {
requestedPasses.push(pipelineValue.value);
}
}
}
}
if (errors.length > 0) {
return {
kind: 'bailout',
content: errors.map(err => {
return err.loc === null || typeof err.loc === 'symbol'
? `React Compiler bailed out:\n\n${err.message}`
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`;
}),
};
}
return {
kind: 'success',
content: [result.code, ...requestedPasses],
};
} catch (err) {
return {
kind: 'error',
text: `Error: ${err.stack}`,
};
}
}

View File

@@ -1,6 +1,6 @@
import puppeteer from 'puppeteer';
export async function parseReactComponentTree(url: string): Promise<string> {
export async function componentTreeTool(url: string): Promise<string> {
try {
const browser = await puppeteer.connect({
browserURL: 'http://127.0.0.1:9222',

View File

@@ -0,0 +1,49 @@
import * as cheerio from 'cheerio';
import {convert} from 'html-to-text';
import {queryAlgolia} from '../utils/algolia';
type DevDocsToolOutput =
| {
kind: 'success';
content: Array<string>;
}
| {
kind: 'error';
text: string;
};
/**
* Tool for querying React dev docs from react.dev
* @param query The search query to look up in the React documentation
* @returns A promise that resolves to the search results
*/
export async function devDocsTool(query: string): Promise<DevDocsToolOutput> {
try {
const pages = await queryAlgolia(query);
if (pages.length === 0) {
return {
kind: 'error',
text: `No results`,
};
}
const content = pages.map(html => {
const $ = cheerio.load(html);
// react.dev should always have at least one <article> with the main content
const article = $('article').html();
if (article != null) {
return convert(article);
} else {
return convert($.html());
}
});
return {
kind: 'success',
content,
};
} catch (err) {
return {
kind: 'error',
text: `Error: ${err.stack}`,
};
}
}

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export * from './runtimePerfTool';
export * from './componentTreeTool';
export * from './compileTool';
export * from './devDocsTool';

View File

@@ -53,7 +53,7 @@ function delay(time: number) {
});
}
export async function measurePerformance(
export async function runtimePerfTool(
code: string,
iterations: number,
): Promise<PerformanceResults> {
@@ -69,23 +69,27 @@ export async function measurePerformance(
throw new Error('Failed to parse code');
}
const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
...babelOptions,
plugins: [
() => ({
visitor: {
ImportDeclaration(
path: babel.NodePath<babel.types.ImportDeclaration>,
) {
const value = path.node.source.value;
if (value === 'react' || value === 'react-dom') {
path.remove();
}
const transformResult = await babel.transformFromAstAsync(
parsed as babel.types.Node,
undefined,
{
...babelOptions,
plugins: [
() => ({
visitor: {
ImportDeclaration(
path: babel.NodePath<babel.types.ImportDeclaration>,
) {
const value = path.node.source.value;
if (value === 'react' || value === 'react-dom') {
path.remove();
}
},
},
},
}),
],
});
}),
],
},
);
const transpiled = transformResult?.code || undefined;
if (!transpiled) {
@@ -125,14 +129,16 @@ export async function measurePerformance(
// ui chaos monkey
const selectors = await page.evaluate(() => {
window.__INTERACTABLE_SELECTORS__ = [];
(window as any).__INTERACTABLE_SELECTORS__ = [];
const elements = Array.from(document.querySelectorAll('a')).concat(
Array.from(document.querySelectorAll('button')),
Array.from(document.querySelectorAll('button')) as any,
);
for (const el of elements) {
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
(window as any).__INTERACTABLE_SELECTORS__.push(
el.tagName.toLowerCase(),
);
}
return window.__INTERACTABLE_SELECTORS__;
return (window as any).__INTERACTABLE_SELECTORS__;
});
await Promise.all(

View File

@@ -0,0 +1,38 @@
{
"name": "react-tools-cli",
"version": "0.0.0",
"description": "CLI to execute react-mcp-server tools in isolation from the full mcp server",
"bin": {
"react-tools-cli": "./dist/index.js"
},
"scripts": {
"build": "rimraf dist && tsup"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git",
"directory": "compiler/packages/react-tools-cli"
},
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/parser": "^7.26",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.27.1",
"@modelcontextprotocol/sdk": "^1.9.0",
"algoliasearch": "^5.23.3",
"cheerio": "^1.0.0",
"html-to-text": "^9.0.5",
"prettier": "^3.3.3",
"puppeteer": "^24.7.2",
"yargs": "^18.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/html-to-text": "^9.0.4",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.3.2"
}
}

View File

@@ -0,0 +1,240 @@
import {
PassNameType,
assertExhaustive,
compileTool,
componentTreeTool,
devDocsTool,
runtimePerfTool,
} from 'react-mcp-server/src';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
yargs(hideBin(process.argv))
.scriptName('react-tools-cli')
.usage('$0 <cmd> [args]')
.command(
'compile [code] [pass-name]',
'Compile React code with React Compiler',
yargs => {
yargs
.positional('code', {
type: 'string',
describe: 'The code to compile',
})
.option('pass-name', {
type: 'string',
choices: ['HIR', 'ReactiveFunction', 'All', '@DEBUG'],
describe: 'Compiler pass to run',
});
},
async function (argv) {
const code: string = String(argv['code'] ?? '');
const passName: PassNameType = argv['pass-name'] as PassNameType;
const results = await compileTool(code, passName);
switch (results.kind) {
case 'success': {
console.log(
JSON.stringify({
isError: false,
content: results.content.map(text => {
return {
type: 'text' as const,
text,
};
}),
}),
);
break;
}
case 'bailout': {
console.log(
JSON.stringify({
isError: true,
content: results.content.map(text => {
return {
type: 'text' as const,
text,
};
}),
}),
);
process.exit(1);
}
case 'error':
case 'compile-error':
console.log(
JSON.stringify({
isError: true,
content: [
{
type: 'text' as const,
text: results.text,
},
],
}),
);
process.exit(1);
default:
assertExhaustive(
results,
`Unhandled result ${JSON.stringify(results)}`,
);
}
},
)
.command(
'query-docs [query]',
'Compile React code with React Compiler',
yargs => {
yargs.positional('query', {
type: 'string',
describe: 'Browse oficcial React documentation for a given query',
});
},
async function (argv) {
const query: string = String(argv['query'] ?? '');
const result = await devDocsTool(query);
switch (result.kind) {
case 'success':
console.log(
JSON.stringify({
isError: false,
content: result.content.map(text => {
return {
type: 'text' as const,
text: text,
};
}),
}),
);
break;
case 'error':
console.log(
JSON.stringify({
isError: true,
content: [{type: 'text' as const, text: result.text}],
}),
);
break;
default:
assertExhaustive(
result,
`Unhandled result ${JSON.stringify(result)}`,
);
}
process.exit(1);
},
)
.command(
'get-component-tree [url]',
'Get the React component tree for a given URL',
yargs => {
yargs.positional('url', {
type: 'string',
default: 'https://localhost:3000',
describe: 'URL for a React App to get the component tree for',
});
},
async function (argv) {
const url: string = String(argv['url']);
try {
const result = await componentTreeTool(url);
console.log(
JSON.stringify({
content: [
{
type: 'text' as const,
text: result,
},
],
}),
);
} catch (err) {
console.log(
JSON.stringify({
isError: true,
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
}),
);
}
process.exit(1);
},
)
.command(
'review-code-runtime [code] [iterations',
'Get the React component tree for a given URL',
yargs => {
yargs.positional('code', {
type: 'string',
default: '',
describe: 'React code to run',
});
yargs.positional('iterations', {
type: 'number',
default: 10,
describe: 'Number of iterations to run the code for',
});
},
async function (argv) {
const code: string = String(argv['code']);
const iterations: number = Number(argv['iterations']);
try {
const results = await runtimePerfTool(code, iterations);
const formattedResults = `
# React Component Performance Results
## Mean Render Time
${calculateMean(results.renderTime)}
## Mean Web Vitals
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
## Mean React Profiler
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
`;
console.log(
JSON.stringify({
content: [
{
type: 'text' as const,
text: formattedResults,
},
],
}),
);
} catch (error) {
console.log(
JSON.stringify({
isError: true,
content: [
{
type: 'text' as const,
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
},
],
}),
);
}
process.exit(1);
},
)
.help()
.parse();
function calculateMean(values: number[]): string {
return values.length > 0
? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms'
: 'could not collect';
}

View File

@@ -0,0 +1,22 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "../",
"noEmit": true,
"jsx": "react-jsxdev",
"lib": ["ES2022"],
// weaken strictness from preset
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"target": "ES2022",
// ideally turn off only during dev, or on a per-file basis
"noUnusedLocals": false,
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts"],
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [],
splitting: false,
sourcemap: false,
dts: false,
bundle: true,
format: 'cjs',
platform: 'node',
target: 'es2022',
banner: {
js: `#!/usr/bin/env node
/**
* 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.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/`,
},
});

View File

@@ -542,11 +542,6 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz"
@@ -1605,7 +1600,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
@@ -1613,14 +1608,6 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
@@ -3745,7 +3732,7 @@ ansi-styles@^5.0.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-styles@^6.1.0:
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
version "6.2.1"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
@@ -4435,6 +4422,15 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
cliui@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-9.0.1.tgz#6f7890f386f6f1f79953adc1f78dec46fcc2d291"
integrity sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==
dependencies:
string-width "^7.2.0"
strip-ansi "^7.1.0"
wrap-ansi "^9.0.0"
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
@@ -4963,7 +4959,7 @@ emittery@^0.13.1:
resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz"
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
emoji-regex@^10.2.1:
emoji-regex@^10.2.1, emoji-regex@^10.3.0:
version "10.4.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz"
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
@@ -5798,6 +5794,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-east-asian-width@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389"
integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
@@ -9825,6 +9826,15 @@ string-width@^6.1.0:
emoji-regex "^10.2.1"
strip-ansi "^7.0.1"
string-width@^7.0.0, string-width@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc"
integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==
dependencies:
emoji-regex "^10.3.0"
get-east-asian-width "^1.0.0"
strip-ansi "^7.1.0"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
@@ -10551,6 +10561,15 @@ wrap-ansi@^8.1.0:
string-width "^5.0.1"
strip-ansi "^7.0.1"
wrap-ansi@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e"
integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==
dependencies:
ansi-styles "^6.2.1"
string-width "^7.0.0"
strip-ansi "^7.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
@@ -10617,6 +10636,11 @@ yargs-parser@^21.0.1, yargs-parser@^21.1.1:
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs-parser@^22.0.0:
version "22.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-22.0.0.tgz#87b82094051b0567717346ecd00fd14804b357c8"
integrity sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==
yargs-unparser@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz"
@@ -10670,6 +10694,18 @@ yargs@^17.3.1, yargs@^17.7.1, yargs@^17.7.2:
y18n "^5.0.5"
yargs-parser "^21.1.1"
yargs@^18.0.0:
version "18.0.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-18.0.0.tgz#6c84259806273a746b09f579087b68a3c2d25bd1"
integrity sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==
dependencies:
cliui "^9.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
string-width "^7.2.0"
y18n "^5.0.5"
yargs-parser "^22.0.0"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"

View File

@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
if (req.headers['cache-control']) {
proxiedHeaders['Cache-Control'] = req.get('cache-control');
}
if (req.get('rsc-request-id')) {
proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
}
const requestsPrerender = req.path === '/prerender';

View File

@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
const React = require('react');
async function renderApp(res, returnValue, formState, noCache) {
const activeDebugChannels =
process.env.NODE_ENV === 'development' ? new Map() : null;
function getDebugChannel(req) {
if (process.env.NODE_ENV !== 'development') {
return undefined;
}
const requestId = req.get('rsc-request-id');
if (!requestId) {
return undefined;
}
return activeDebugChannels.get(requestId);
}
async function renderApp(
res,
returnValue,
formState,
noCache,
promiseForDebugChannel
) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap);
const {pipe} = renderToPipeableStream(payload, moduleMap, {
debugChannel: await promiseForDebugChannel,
});
pipe(res);
}
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
if ('prerender' in req.query) {
await prerenderApp(res, null, null, noCache);
} else {
await renderApp(res, null, null, noCache);
await renderApp(res, null, null, noCache, getDebugChannel(req));
}
});
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result, null, noCache);
renderApp(res, result, null, noCache, getDebugChannel(req));
} else {
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
// Wait for any mutations
const result = await action();
const formState = decodeFormState(result, formData);
renderApp(res, null, formState, noCache);
renderApp(res, null, formState, noCache, undefined);
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
renderApp(res, null, null, noCache);
renderApp(res, null, null, noCache, undefined);
}
}
});
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
});
}
app.listen(3001, () => {
const httpServer = app.listen(3001, () => {
console.log('Regional Flight Server listening on port 3001...');
});
@@ -346,3 +368,27 @@ app.on('error', function (error) {
throw error;
}
});
if (process.env.NODE_ENV === 'development') {
// Open a websocket server for Debug information
const WebSocket = require('ws');
const webSocketServer = new WebSocket.Server({noServer: true});
httpServer.on('upgrade', (request, socket, head) => {
const DEBUG_CHANNEL_PATH = '/debug-channel?';
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
const promiseForWs = new Promise(resolve => {
webSocketServer.handleUpgrade(request, socket, head, ws => {
ws.on('close', () => {
activeDebugChannels.delete(requestId);
});
resolve(ws);
});
});
activeDebugChannels.set(requestId, promiseForWs);
} else {
socket.destroy();
}
});
}

View File

@@ -33,12 +33,22 @@ function Foo({children}) {
return <div>{children}</div>;
}
async function delayedError(text, ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(text)), ms)
);
}
async function delay(text, ms) {
return new Promise(resolve => setTimeout(() => resolve(text), ms));
}
async function delayTwice() {
await delay('', 20);
try {
await delayedError('Delayed exception', 20);
} catch (x) {
// Ignored
}
await delay('', 10);
}

View File

@@ -42,17 +42,43 @@ function Shell({data}) {
}
async function hydrateApp() {
const {root, returnValue, formState} = await createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer,
findSourceMapURL,
}
);
let response;
if (
process.env.NODE_ENV === 'development' &&
typeof WebSocketStream === 'function'
) {
const requestId = crypto.randomUUID();
const wss = new WebSocketStream(
'ws://localhost:3001/debug-channel?' + requestId
);
const debugChannel = await wss.opened;
response = createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
'rsc-request-id': requestId,
},
}),
{
callServer,
debugChannel,
findSourceMapURL,
}
);
} else {
response = createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer,
findSourceMapURL,
}
);
}
const {root, returnValue, formState} = await response;
ReactDOM.hydrateRoot(
document,

View File

@@ -4,7 +4,6 @@ import React, {
useEffect,
useState,
unstable_addTransitionType as addTransitionType,
use,
} from 'react';
import Chrome from './Chrome';

View File

@@ -79,7 +79,9 @@ import {
logDedupedComponentRender,
logComponentErrored,
logIOInfo,
logIOInfoErrored,
logComponentAwait,
logComponentAwaitErrored,
} from './ReactFlightPerformanceTrack';
import {
@@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
import {injectInternals} from './ReactFlightClientDevToolsHook';
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
import ReactVersion from 'shared/ReactVersion';
import isArray from 'shared/isArray';
@@ -155,12 +159,12 @@ const RESOLVED_MODEL = 'resolved_model';
const RESOLVED_MODULE = 'resolved_module';
const INITIALIZED = 'fulfilled';
const ERRORED = 'rejected';
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
type PendingChunk<T> = {
status: 'pending',
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -169,7 +173,6 @@ type BlockedChunk<T> = {
status: 'blocked',
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -177,8 +180,7 @@ type BlockedChunk<T> = {
type ResolvedModelChunk<T> = {
status: 'resolved_model',
value: UninitializedModel,
reason: null,
_response: Response,
reason: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -187,7 +189,6 @@ type ResolvedModuleChunk<T> = {
status: 'resolved_module',
value: ClientReference<T>,
reason: null,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -196,7 +197,6 @@ type InitializedChunk<T> = {
status: 'fulfilled',
value: T,
reason: null | FlightStreamController,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -207,7 +207,6 @@ type InitializedStreamChunk<
status: 'fulfilled',
value: T,
reason: FlightStreamController,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
@@ -216,7 +215,14 @@ type ErroredChunk<T> = {
status: 'rejected',
value: null,
reason: mixed,
_response: Response,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type HaltedChunk<T> = {
status: 'halted',
value: null,
reason: null,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
@@ -227,19 +233,14 @@ type SomeChunk<T> =
| ResolvedModelChunk<T>
| ResolvedModuleChunk<T>
| InitializedChunk<T>
| ErroredChunk<T>;
| ErroredChunk<T>
| HaltedChunk<T>;
// $FlowFixMe[missing-this-annot]
function ReactPromise(
status: any,
value: any,
reason: any,
response: Response,
) {
function ReactPromise(status: any, value: any, reason: any) {
this.status = status;
this.value = value;
this.reason = reason;
this._response = response;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
this._children = [];
}
@@ -266,7 +267,11 @@ ReactPromise.prototype.then = function <T>(
initializeModuleChunk(chunk);
break;
}
if (__DEV__ && enableAsyncDebugInfo) {
if (
__DEV__ &&
enableAsyncDebugInfo &&
(typeof resolve !== 'function' || !(resolve: any).isReactInternalListener)
) {
// Because only native Promises get picked up when we're awaiting we need to wrap
// this in a native Promise in DEV. This means that these callbacks are no longer sync
// but the lazy initialization is still sync and the .value can be inspected after,
@@ -307,6 +312,9 @@ ReactPromise.prototype.then = function <T>(
chunk.reason.push(reject);
}
break;
case HALTED: {
break;
}
default:
if (reject) {
reject(chunk.reason);
@@ -320,6 +328,8 @@ export type FindSourceMapURLCallback = (
environmentName: string,
) => null | string;
export type DebugChannelCallback = (message: string) => void;
export type Response = {
_bundlerConfig: ServerConsumerModuleMap,
_serverReferenceConfig: null | ServerManifest,
@@ -343,6 +353,7 @@ export type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannelCallback, // DEV-only
_replayConsole: boolean, // DEV-only
_rootEnvironmentName: string, // DEV-only, the requested environment name.
};
@@ -364,6 +375,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
return chunk.value;
case PENDING:
case BLOCKED:
case HALTED:
// eslint-disable-next-line no-throw-literal
throw ((chunk: any): Thenable<T>);
default:
@@ -378,12 +390,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
function createPendingChunk<T>(response: Response): PendingChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(PENDING, null, null, response);
return new ReactPromise(PENDING, null, null);
}
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(BLOCKED, null, null, response);
return new ReactPromise(BLOCKED, null, null);
}
function createErrorChunk<T>(
@@ -391,7 +403,7 @@ function createErrorChunk<T>(
error: mixed,
): ErroredChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(ERRORED, null, error, response);
return new ReactPromise(ERRORED, null, error);
}
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
@@ -463,7 +475,7 @@ function createResolvedModelChunk<T>(
value: UninitializedModel,
): ResolvedModelChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODEL, value, null, response);
return new ReactPromise(RESOLVED_MODEL, value, response);
}
function createResolvedModuleChunk<T>(
@@ -471,7 +483,7 @@ function createResolvedModuleChunk<T>(
value: ClientReference<T>,
): ResolvedModuleChunk<T> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODULE, value, null, response);
return new ReactPromise(RESOLVED_MODULE, value, null);
}
function createInitializedTextChunk(
@@ -479,7 +491,7 @@ function createInitializedTextChunk(
value: string,
): InitializedChunk<string> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, null, response);
return new ReactPromise(INITIALIZED, value, null);
}
function createInitializedBufferChunk(
@@ -487,7 +499,7 @@ function createInitializedBufferChunk(
value: $ArrayBufferView | ArrayBuffer,
): InitializedChunk<Uint8Array> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, null, response);
return new ReactPromise(INITIALIZED, value, null);
}
function createInitializedIteratorResultChunk<T>(
@@ -496,12 +508,7 @@ function createInitializedIteratorResultChunk<T>(
done: boolean,
): InitializedChunk<IteratorResult<T, T>> {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: done, value: value},
null,
response,
);
return new ReactPromise(INITIALIZED, {done: done, value: value}, null);
}
function createInitializedStreamChunk<
@@ -514,7 +521,7 @@ function createInitializedStreamChunk<
// We use the reason field to stash the controller since we already have that
// field. It's a bit of a hack but efficient.
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(INITIALIZED, value, controller, response);
return new ReactPromise(INITIALIZED, value, controller);
}
function createResolvedIteratorResultChunk<T>(
@@ -526,10 +533,11 @@ function createResolvedIteratorResultChunk<T>(
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response);
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response);
}
function resolveIteratorResultChunk<T>(
response: Response,
chunk: SomeChunk<IteratorResult<T, T>>,
value: UninitializedModel,
done: boolean,
@@ -537,10 +545,11 @@ function resolveIteratorResultChunk<T>(
// To reuse code as much code as possible we add the wrapper element as part of the JSON.
const iteratorResultJSON =
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
resolveModelChunk(chunk, iteratorResultJSON);
resolveModelChunk(response, chunk, iteratorResultJSON);
}
function resolveModelChunk<T>(
response: Response,
chunk: SomeChunk<T>,
value: UninitializedModel,
): void {
@@ -557,6 +566,7 @@ function resolveModelChunk<T>(
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED_MODEL;
resolvedChunk.value = value;
resolvedChunk.reason = response;
if (resolveListeners !== null) {
// This is unfortunate that we're reading this eagerly if
// we already have listeners attached since they might no
@@ -602,6 +612,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
initializingHandler = null;
const resolvedModel = chunk.value;
const response = chunk.reason;
// We go to the BLOCKED state until we've fully resolved this.
// We do this before parsing in case we try to initialize the same chunk
@@ -616,7 +627,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
}
try {
const value: T = parseModel(chunk._response, resolvedModel);
const value: T = parseModel(response, resolvedModel);
// Invoke any listeners added while resolving this model. I.e. cyclic
// references. This may or may not fully resolve the model depending on
// if they were blocked.
@@ -679,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
triggerErrorOnChunk(chunk, error);
}
});
if (__DEV__) {
const debugChannel = response._debugChannel;
if (debugChannel !== undefined) {
// If we don't have any more ways of reading data, we don't have to send any
// more neither. So we close the writable side.
debugChannel('');
response._debugChannel = undefined;
}
}
if (enableProfilerTimer && enableComponentPerformanceTrack) {
markAllTracksInOrder();
flushComponentPerformance(
@@ -1052,6 +1072,10 @@ function waitForReference<T>(
}
}
}
// Use to avoid the microtask resolution in DEV.
if (__DEV__ && enableAsyncDebugInfo) {
(fulfill: any).isReactInternalListener = true;
}
function reject(error: mixed): void {
if (handler.errored) {
@@ -1359,6 +1383,7 @@ function getOutlinedModel<T>(
return chunkValue;
case PENDING:
case BLOCKED:
case HALTED:
return waitForReference(chunk, parentObject, key, response, map, path);
default:
// This is an error. Instead of erroring directly, we're going to encode this on
@@ -1406,6 +1431,17 @@ function createFormData(
return formData;
}
function applyConstructor(
response: Response,
model: Function,
parentObject: Object,
key: string,
): void {
Object.setPrototypeOf(parentObject, model.prototype);
// Delete the property. It was just a placeholder.
return undefined;
}
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
return model[Symbol.iterator]();
@@ -1462,10 +1498,6 @@ function parseModelString(
}
case '@': {
// Promise
if (value.length === 2) {
// Infinite promise that never resolves.
return new Promise(() => {});
}
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
@@ -1586,16 +1618,60 @@ function parseModelString(
// BigInt
return BigInt(value.slice(2));
}
case 'P': {
if (__DEV__) {
// In DEV mode we allow debug objects to specify themselves as instances of
// another constructor.
const ref = value.slice(2);
return getOutlinedModel(
response,
ref,
parentObject,
key,
applyConstructor,
);
}
//Fallthrough
}
case 'E': {
if (__DEV__) {
// In DEV mode we allow indirect eval to produce functions for logging.
// This should not compile to eval() because then it has local scope access.
const code = value.slice(2);
try {
// eslint-disable-next-line no-eval
return (0, eval)(value.slice(2));
return (0, eval)(code);
} catch (x) {
// We currently use this to express functions so we fail parsing it,
// let's just return a blank function as a place holder.
if (code.startsWith('(async function')) {
const idx = code.indexOf('(', 15);
if (idx !== -1) {
const name = code.slice(15, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':async function(){}})',
)[name];
}
} else if (code.startsWith('(function')) {
const idx = code.indexOf('(', 9);
if (idx !== -1) {
const name = code.slice(9, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)(
'({' + JSON.stringify(name) + ':function(){}})',
)[name];
}
} else if (code.startsWith('(class')) {
const idx = code.indexOf('{', 6);
if (idx !== -1) {
const name = code.slice(6, idx).trim();
// eslint-disable-next-line no-eval
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
name
];
}
}
return function () {};
}
}
@@ -1603,17 +1679,21 @@ function parseModelString(
}
case 'Y': {
if (__DEV__) {
if (value.length > 2) {
const debugChannel = response._debugChannel;
if (debugChannel) {
const ref = value.slice(2);
debugChannel('R:' + ref); // Release this reference immediately
}
}
// In DEV mode we encode omitted objects in logs as a getter that throws
// so that when you try to access it on the client, you know why that
// happened.
Object.defineProperty(parentObject, key, {
get: function () {
// TODO: We should ideally throw here to indicate a difference.
return (
'This object has been omitted by React in the console log ' +
'to avoid sending too much data from the server. Try logging smaller ' +
'or more specific objects.'
);
return OMITTED_PROP_ERROR;
},
enumerable: true,
configurable: false,
@@ -1670,9 +1750,10 @@ function ResponseInstance(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
findSourceMapURL: void | FindSourceMapURLCallback,
replayConsole: boolean,
environmentName: void | string,
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
) {
const chunks: Map<number, SomeChunk<any>> = new Map();
this._bundlerConfig = bundlerConfig;
@@ -1727,6 +1808,7 @@ function ResponseInstance(
);
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
this._replayConsole = replayConsole;
this._rootEnvironmentName = rootEnv;
}
@@ -1742,9 +1824,10 @@ export function createResponse(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
findSourceMapURL: void | FindSourceMapURLCallback,
replayConsole: boolean,
environmentName: void | string,
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
replayConsole: boolean, // DEV-only
environmentName: void | string, // DEV-only
debugChannel: void | DebugChannelCallback, // DEV-only
): Response {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
return new ResponseInstance(
@@ -1758,9 +1841,26 @@ export function createResponse(
findSourceMapURL,
replayConsole,
environmentName,
debugChannel,
);
}
function resolveDebugHalt(response: Response, id: number): void {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
chunks.set(id, (chunk = createPendingChunk(response)));
} else {
}
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
return;
}
const haltedChunk: HaltedChunk<any> = (chunk: any);
haltedChunk.status = HALTED;
haltedChunk.value = null;
haltedChunk.reason = null;
}
function resolveModel(
response: Response,
id: number,
@@ -1771,7 +1871,7 @@ function resolveModel(
if (!chunk) {
chunks.set(id, createResolvedModelChunk(response, model));
} else {
resolveModelChunk(chunk, model);
resolveModelChunk(response, chunk, model);
}
}
@@ -1945,7 +2045,7 @@ function startReadableStream<T>(
// to synchronous emitting.
previousBlockedChunk = null;
}
resolveModelChunk(chunk, json);
resolveModelChunk(response, chunk, json);
});
}
},
@@ -2033,7 +2133,12 @@ function startAsyncIterable<T>(
false,
);
} else {
resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
false,
);
}
nextWriteIndex++;
},
@@ -2046,12 +2151,18 @@ function startAsyncIterable<T>(
true,
);
} else {
resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex],
value,
true,
);
}
nextWriteIndex++;
while (nextWriteIndex < buffer.length) {
// In generators, any extra reads from the iterator have the value undefined.
resolveIteratorResultChunk(
response,
buffer[nextWriteIndex++],
'"$undefined"',
true,
@@ -2069,32 +2180,33 @@ function startAsyncIterable<T>(
}
},
};
const iterable: $AsyncIterable<T, T, void> = {
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
let nextReadIndex = 0;
return createIterator(arg => {
if (arg !== undefined) {
throw new Error(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
const iterable: $AsyncIterable<T, T, void> = ({}: any);
// $FlowFixMe[cannot-write]
iterable[ASYNC_ITERATOR] = (): $AsyncIterator<T, T, void> => {
let nextReadIndex = 0;
return createIterator(arg => {
if (arg !== undefined) {
throw new Error(
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
);
}
if (nextReadIndex === buffer.length) {
if (closed) {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: true, value: undefined},
null,
);
}
if (nextReadIndex === buffer.length) {
if (closed) {
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
return new ReactPromise(
INITIALIZED,
{done: true, value: undefined},
null,
response,
);
}
buffer[nextReadIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
return buffer[nextReadIndex++];
});
},
buffer[nextReadIndex] =
createPendingChunk<IteratorResult<T, T>>(response);
}
return buffer[nextReadIndex++];
});
};
// TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after
// reading through the end, but currently we favor code size over this optimization.
resolveStream(
@@ -2627,9 +2739,15 @@ function initializeFakeStack(
// $FlowFixMe[cannot-write]
debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
}
if (debugInfo.owner != null) {
const owner = debugInfo.owner;
if (owner != null) {
// Initialize any owners not yet initialized.
initializeFakeStack(response, debugInfo.owner);
initializeFakeStack(response, owner);
if (owner.debugLocation === undefined && debugInfo.debugStack != null) {
// If we are the child of this owner, then the owner should be the bottom frame
// our stack. We can use it as the implied location of the owner.
owner.debugLocation = debugInfo.debugStack;
}
}
}
@@ -2816,7 +2934,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
// $FlowFixMe[cannot-write]
ioInfo.end += response._timeOrigin;
logIOInfo(ioInfo, response._rootEnvironmentName);
const env = response._rootEnvironmentName;
const promise = ioInfo.value;
if (promise) {
const thenable: Thenable<mixed> = (promise: any);
switch (thenable.status) {
case INITIALIZED:
logIOInfo(ioInfo, env, thenable.value);
break;
case ERRORED:
logIOInfoErrored(ioInfo, env, thenable.reason);
break;
default:
// If we haven't resolved the Promise yet, wait to log until have so we can include
// its data in the log.
promise.then(
logIOInfo.bind(null, ioInfo, env),
logIOInfoErrored.bind(null, ioInfo, env),
);
break;
}
} else {
logIOInfo(ioInfo, env, undefined);
}
}
function resolveIOInfo(
@@ -2831,7 +2971,7 @@ function resolveIOInfo(
chunks.set(id, chunk);
initializeModelChunk(chunk);
} else {
resolveModelChunk(chunk, model);
resolveModelChunk(response, chunk, model);
if (chunk.status === RESOLVED_MODEL) {
initializeModelChunk(chunk);
}
@@ -3100,13 +3240,55 @@ function flushComponentPerformance(
}
// $FlowFixMe: Refined.
const asyncInfo: ReactAsyncInfo = candidateInfo;
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
response._rootEnvironmentName,
);
const env = response._rootEnvironmentName;
const promise = asyncInfo.awaited.value;
if (promise) {
const thenable: Thenable<mixed> = (promise: any);
switch (thenable.status) {
case INITIALIZED:
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
thenable.value,
);
break;
case ERRORED:
logComponentAwaitErrored(
asyncInfo,
trackIdx,
time,
endTime,
env,
thenable.reason,
);
break;
default:
// We assume that we should have received the data by now since this is logged at the
// end of the response stream. This is more sensitive to ordering so we don't wait
// to log it.
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
undefined,
);
break;
}
} else {
logComponentAwait(
asyncInfo,
trackIdx,
time,
endTime,
env,
undefined,
);
}
}
}
}
@@ -3329,6 +3511,10 @@ function processFullStringRow(
}
// Fallthrough
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
if (__DEV__ && row === '') {
resolveDebugHalt(response, id);
return;
}
// We assume anything else is JSON.
resolveModel(response, id, row);
return;

View File

@@ -17,14 +17,143 @@ import type {
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';
const supportsUserTiming =
enableProfilerTimer &&
typeof console !== 'undefined' &&
typeof console.timeStamp === 'function';
typeof console.timeStamp === 'function' &&
typeof performance !== 'undefined' &&
// $FlowFixMe[method-unbinding]
typeof performance.measure === 'function';
const IO_TRACK = 'Server Requests ⚛';
const COMPONENTS_TRACK = 'Server Components ⚛';
const EMPTY_ARRAY = 0;
const COMPLEX_ARRAY = 1;
const PRIMITIVE_ARRAY = 2; // Primitive values only
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
let kind = EMPTY_ARRAY;
for (let i = 0; i < array.length; i++) {
const value = array[i];
if (typeof value === 'object' && value !== null) {
if (
isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string'
) {
// Key value tuple
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
return COMPLEX_ARRAY;
}
kind = ENTRIES_ARRAY;
} else {
return COMPLEX_ARRAY;
}
} else if (typeof value === 'function') {
return COMPLEX_ARRAY;
} else if (typeof value === 'string' && value.length > 50) {
return COMPLEX_ARRAY;
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
return COMPLEX_ARRAY;
} else {
kind = PRIMITIVE_ARRAY;
}
}
return kind;
}
function addObjectToProperties(
object: Object,
properties: Array<[string, string]>,
indent: number,
): void {
for (const key in object) {
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
const value = object[key];
addValueToProperties(key, value, properties, indent);
}
}
}
function addValueToProperties(
propertyName: string,
value: mixed,
properties: Array<[string, string]>,
indent: number,
): void {
let desc;
switch (typeof value) {
case 'object':
if (value === null) {
desc = 'null';
break;
} else {
// $FlowFixMe[method-unbinding]
const objectToString = Object.prototype.toString.call(value);
let objectName = objectToString.slice(8, objectToString.length - 1);
if (objectName === 'Array') {
const array: Array<any> = (value: any);
const kind = getArrayKind(array);
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
desc = JSON.stringify(array);
break;
} else if (kind === ENTRIES_ARRAY) {
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
for (let i = 0; i < array.length; i++) {
const entry = array[i];
addValueToProperties(entry[0], entry[1], properties, indent + 1);
}
return;
}
}
if (objectName === 'Object') {
const proto: any = Object.getPrototypeOf(value);
if (proto && typeof proto.constructor === 'function') {
objectName = proto.constructor.name;
}
}
properties.push([
'\xa0\xa0'.repeat(indent) + propertyName,
objectName === 'Object' ? '' : objectName,
]);
if (indent < 3) {
addObjectToProperties(value, properties, indent + 1);
}
return;
}
case 'function':
if (value.name === '') {
desc = '() => {}';
} else {
desc = value.name + '() {}';
}
break;
case 'string':
if (value === OMITTED_PROP_ERROR) {
desc = '...';
} else {
desc = JSON.stringify(value);
}
break;
case 'undefined':
desc = 'undefined';
break;
case 'boolean':
desc = value ? 'true' : 'false';
break;
default:
// eslint-disable-next-line react-internal/safe-string-coercion
desc = String(value);
}
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
}
export function markAllTracksInOrder() {
if (supportsUserTiming) {
// Ensure we create the Server Component track groups earlier than the Client Scheduler
@@ -133,12 +262,7 @@ export function logComponentErrored(
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
if (
__DEV__ &&
typeof performance !== 'undefined' &&
// $FlowFixMe[method-unbinding]
typeof performance.measure === 'function'
) {
if (__DEV__) {
const message =
typeof error === 'object' &&
error !== null &&
@@ -228,12 +352,68 @@ function getIOColor(
}
}
export function logComponentAwaitErrored(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
error: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const env = asyncInfo.env;
const name = asyncInfo.awaited.name;
const isPrimaryEnv = env === rootEnv;
const entryName =
'await ' +
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
const debugTask = asyncInfo.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Rejected', message]];
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
tooltipText: entryName + ' Rejected',
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
'error',
);
}
}
}
export function logComponentAwait(
asyncInfo: ReactAsyncInfo,
trackIdx: number,
startTime: number,
endTime: number,
rootEnv: string,
value: mixed,
): void {
if (supportsUserTiming && endTime > 0) {
const env = asyncInfo.env;
@@ -245,17 +425,26 @@ export function logComponentAwait(
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
const debugTask = asyncInfo.debugTask;
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0);
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0);
}
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
entryName,
startTime < 0 ? 0 : startTime,
endTime,
trackNames[trackIdx],
COMPONENTS_TRACK,
color,
),
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: trackNames[trackIdx],
trackGroup: COMPONENTS_TRACK,
properties,
},
},
}),
);
} else {
console.timeStamp(
@@ -270,7 +459,63 @@ export function logComponentAwait(
}
}
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
export function logIOInfoErrored(
ioInfo: ReactIOInfo,
rootEnv: string,
error: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
const name = ioInfo.name;
const env = ioInfo.env;
const isPrimaryEnv = env === rootEnv;
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
const debugTask = ioInfo.debugTask;
if (__DEV__ && debugTask) {
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Rejected', message]];
debugTask.run(
// $FlowFixMe[method-unbinding]
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: IO_TRACK,
properties,
tooltipText: entryName + ' Rejected',
},
},
}),
);
} else {
console.timeStamp(
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
'error',
);
}
}
}
export function logIOInfo(
ioInfo: ReactIOInfo,
rootEnv: string,
value: mixed,
): void {
const startTime = ioInfo.start;
const endTime = ioInfo.end;
if (supportsUserTiming && endTime >= 0) {
@@ -282,17 +527,25 @@ export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
const debugTask = ioInfo.debugTask;
const color = getIOColor(name);
if (__DEV__ && debugTask) {
const properties: Array<[string, string]> = [];
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0);
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0);
}
debugTask.run(
// $FlowFixMe[method-unbinding]
console.timeStamp.bind(
console,
entryName,
startTime < 0 ? 0 : startTime,
endTime,
IO_TRACK,
undefined,
color,
),
performance.measure.bind(performance, entryName, {
start: startTime < 0 ? 0 : startTime,
end: endTime,
detail: {
devtools: {
color: color,
track: IO_TRACK,
properties,
},
},
}),
);
} else {
console.timeStamp(

View File

@@ -0,0 +1,13 @@
/**
* 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 OMITTED_PROP_ERROR =
'This object has been omitted by React in the console log ' +
'to avoid sending too much data from the server. Try logging smaller ' +
'or more specific objects.';

View File

@@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) {
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
@@ -3208,12 +3208,29 @@ describe('ReactFlight', () => {
return 'hello';
}
class MyClass {
constructor() {
this.x = 1;
}
method() {}
get y() {
return this.x + 1;
}
get z() {
return this.x + 5;
}
}
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
function ServerComponent() {
console.log('hi', {
prop: 123,
fn: foo,
map: new Map([['foo', foo]]),
promise: new Promise(() => {}),
promise: Promise.resolve('yo'),
infinitePromise: new Promise(() => {}),
Class: MyClass,
instance: new MyClass(),
});
throw new Error('err');
}
@@ -3258,9 +3275,14 @@ describe('ReactFlight', () => {
});
ownerStacks = [];
// Let the Promises resolve.
await 0;
await 0;
await 0;
// The error should not actually get logged because we're not awaiting the root
// so it's not thrown but the server log also shouldn't be replayed.
await ReactNoopFlightClient.read(transport);
await ReactNoopFlightClient.read(transport, {close: true});
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
@@ -3276,9 +3298,40 @@ describe('ReactFlight', () => {
expect(typeof loggedFn2).toBe('function');
expect(loggedFn2).not.toBe(foo);
expect(loggedFn2.toString()).toBe(foo.toString());
expect(loggedFn2).toBe(loggedFn);
const promise = mockConsoleLog.mock.calls[0][1].promise;
expect(promise).toBeInstanceOf(Promise);
expect(await promise).toBe('yo');
const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
expect(infinitePromise).toBeInstanceOf(Promise);
let resolved = false;
infinitePromise.then(
() => (resolved = true),
x => {
console.error(x);
resolved = true;
},
);
await 0;
await 0;
await 0;
// This should not reject upon aborting the stream.
expect(resolved).toBe(false);
const Class = mockConsoleLog.mock.calls[0][1].Class;
const instance = mockConsoleLog.mock.calls[0][1].instance;
expect(typeof Class).toBe('function');
expect(Class.prototype.constructor).toBe(Class);
expect(instance instanceof Class).toBe(true);
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
expect(instance.x).toBe(1);
expect(instance.hasOwnProperty('y')).toBe(true);
expect(instance.y).toBe(2); // Enumerable getter was reified
expect(instance.hasOwnProperty('z')).toBe(false);
expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
expect(ownerStacks).toEqual(['\n in App (at **)']);
});
@@ -3327,19 +3380,10 @@ describe('ReactFlight', () => {
await ReactNoopFlightClient.read(transport);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
// TODO: Support cyclic objects in console encoding.
// expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
// const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
// expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
// expect(cyclic2.cycle).toBe(cyclic2);
expect(mockConsoleLog.mock.calls[0][0]).toBe(
'Unknown Value: React could not send it from the server.',
);
expect(mockConsoleLog.mock.calls[0][1].message).toBe(
'Converting circular structure to JSON\n' +
" --> starting at object with constructor 'Object'\n" +
" --- property 'cycle' closes the circle",
);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
expect(cyclic2.cycle).toBe(cyclic2);
});
// @gate !__DEV__ || enableComponentPerformanceTrack

View File

@@ -0,0 +1,139 @@
/**
* 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.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('undici').File;
global.FormData = require('undici').FormData;
}
function formatV8Stack(stack) {
let v8StyleStack = '';
if (stack) {
for (let i = 0; i < stack.length; i++) {
const [name] = stack[i];
if (v8StyleStack !== '') {
v8StyleStack += '\n';
}
v8StyleStack += ' in ' + name + ' (at **)';
}
}
return v8StyleStack;
}
function normalizeComponentInfo(debugInfo) {
if (Array.isArray(debugInfo.stack)) {
const {debugTask, debugStack, ...copy} = debugInfo;
copy.stack = formatV8Stack(debugInfo.stack);
if (debugInfo.owner) {
copy.owner = normalizeComponentInfo(debugInfo.owner);
}
return copy;
} else {
return debugInfo;
}
}
function getDebugInfo(obj) {
const debugInfo = obj._debugInfo;
if (debugInfo) {
const copy = [];
for (let i = 0; i < debugInfo.length; i++) {
copy.push(normalizeComponentInfo(debugInfo[i]));
}
return copy;
}
return debugInfo;
}
let act;
let React;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
describe('ReactFlight', () => {
beforeEach(() => {
// Mock performance.now for timing tests
let time = 10;
const now = jest.fn().mockImplementation(() => {
return time++;
});
Object.defineProperty(performance, 'timeOrigin', {
value: time,
configurable: true,
});
Object.defineProperty(performance, 'now', {
value: now,
configurable: true,
});
jest.resetModules();
jest.mock('react', () => require('react/react.react-server'));
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
// This stores the state so we need to preserve it
const flightModules = require('react-noop-renderer/flight-modules');
jest.resetModules();
__unmockReact();
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
React = require('react');
ReactNoop = require('react-noop-renderer');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = require('internal-test-utils').act;
});
afterEach(() => {
jest.restoreAllMocks();
});
// @gate __DEV__ && enableComponentPerformanceTrack
it('can render deep but cut off JSX in debug info', async () => {
function createDeepJSX(n) {
if (n <= 0) {
return null;
}
return <div>{createDeepJSX(n - 1)}</div>;
}
function ServerComponent(props) {
return <div>not using props</div>;
}
const debugChannel = {onMessage(message) {}};
const transport = ReactNoopFlightServer.render(
{
root: (
<ServerComponent>
{createDeepJSX(100) /* deper than objectLimit */}
</ServerComponent>
),
},
{debugChannel},
);
await act(async () => {
const rootModel = await ReactNoopFlightClient.read(transport, {
debugChannel,
});
const root = rootModel.root;
const children = getDebugInfo(root)[1].props.children;
expect(children.type).toBe('div');
expect(children.props.children.type).toBe('div');
ReactNoop.render(root);
});
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
});
});

View File

@@ -5831,13 +5831,21 @@ export function attach(
}
function getSourceForInstance(instance: DevToolsInstance): Source | null {
const unresolvedSource = instance.source;
let unresolvedSource = instance.source;
if (unresolvedSource === null) {
// We don't have any source yet. We can try again later in case an owned child mounts later.
// TODO: We won't have any information here if the child is filtered.
return null;
}
if (instance.kind === VIRTUAL_INSTANCE) {
// We might have found one on the virtual instance.
const debugLocation = instance.data.debugLocation;
if (debugLocation != null) {
unresolvedSource = debugLocation;
}
}
// If we have the debug stack (the creation stack of the JSX) for any owned child of this
// component, then at the bottom of that stack will be a stack frame that is somewhere within
// the component's function body. Typically it would be the callsite of the JSX unless there's

View File

@@ -6,7 +6,7 @@ export const markShellTime =
export const clientRenderBoundary =
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
export const completeBoundary =
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};\n$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
export const completeBoundaryUpgradeToViewTransitions =
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
export const completeBoundaryWithStyles =

View File

@@ -34,11 +34,16 @@ export function revealCompletedBoundaries(batch) {
for (let i = 0; i < batch.length; i += 2) {
const suspenseIdNode = batch[i];
const contentNode = batch[i + 1];
// We can detach the content now.
// Completions of boundaries within this contentNode will now find the boundary
// in its designated place.
contentNode.parentNode.removeChild(contentNode);
if (contentNode.parentNode === null) {
// If the client has failed hydration we may have already deleted the streaming
// segments. The server may also have emitted a complete instruction but cancelled
// the segment. Regardless we can ignore this case.
} else {
// We can detach the content now.
// Completions of boundaries within this contentNode will now find the boundary
// in its designated place.
contentNode.parentNode.removeChild(contentNode);
}
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.

View File

@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
undefined,
'Markup',
undefined,
false,
);
const flightResponse = createFlightResponse(
null,

View File

@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
const decoderOptions = {stream: true};
const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
createStringDecoder() {
return new TextDecoder();
},
@@ -56,6 +56,8 @@ const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
type ReadOptions = {|
findSourceMapURL?: FindSourceMapURLCallback,
debugChannel?: {onMessage: (message: string) => void},
close?: boolean,
|};
function read<T>(source: Source, options: ReadOptions): Thenable<T> {
@@ -70,10 +72,16 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
options !== undefined ? options.findSourceMapURL : undefined,
true,
undefined,
__DEV__ && options !== undefined && options.debugChannel !== undefined
? options.debugChannel.onMessage
: undefined,
);
for (let i = 0; i < source.length; i++) {
processBinaryChunk(response, source[i], 0);
}
if (options !== undefined && options.close) {
close(response);
}
return getRoot(response);
}

View File

@@ -71,6 +71,7 @@ type Options = {
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
signal?: AbortSignal,
debugChannel?: {onMessage?: (message: string) => void},
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
};
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
__DEV__ && options && options.debugChannel !== undefined,
);
const signal = options ? options.signal : undefined;
if (signal) {
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
signal.addEventListener('abort', listener);
}
}
if (__DEV__ && options && options.debugChannel !== undefined) {
options.debugChannel.onMessage = message => {
ReactNoopFlightServer.resolveDebugMessage(request, message);
};
}
ReactNoopFlightServer.startWork(request);
ReactNoopFlightServer.startFlowing(request, destination);
return destination;

View File

@@ -236,7 +236,7 @@ import {
isContextProvider as isLegacyContextProvider,
pushTopLevelContextObject,
invalidateContextProvider,
} from './ReactFiberContext';
} from './ReactFiberLegacyContext';
import {
getIsHydrating,
enterHydrationState,

View File

@@ -53,7 +53,7 @@ import {
getUnmaskedContext,
hasContextChanged,
emptyContextObject,
} from './ReactFiberContext';
} from './ReactFiberLegacyContext';
import {readContext, checkIfContextChanged} from './ReactFiberNewContext';
import {requestUpdateLane, scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
import {
@@ -695,7 +695,7 @@ function constructClassInstance(
}
// Cache unmasked context so we can avoid recreating masked context unless necessary.
// ReactFiberContext usually updates this cache but can't for newly-created instances.
// ReactFiberLegacyContext usually updates this cache but can't for newly-created instances.
if (isLegacyContextConsumer) {
cacheContext(workInProgress, unmaskedContext, context);
}

View File

@@ -150,7 +150,7 @@ import {
isContextProvider as isLegacyContextProvider,
popContext as popLegacyContext,
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext';
} from './ReactFiberLegacyContext';
import {popProvider} from './ReactFiberNewContext';
import {
prepareToHydrateHostInstance,

View File

@@ -79,7 +79,11 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
for (let i = debugInfo.length - 1; i >= 0; i--) {
const entry = debugInfo[i];
if (typeof entry.name === 'string') {
info += describeDebugInfoFrame(entry.name, entry.env);
info += describeDebugInfoFrame(
entry.name,
entry.env,
entry.debugLocation,
);
}
}
}

View File

@@ -20,7 +20,7 @@ import {
} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
import {updateContainerSync} from './ReactFiberReconciler';
import {emptyContextObject} from './ReactFiberContext';
import {emptyContextObject} from './ReactFiberLegacyContext';
import {SyncLane} from './ReactFiberLane';
import {
ClassComponent,

View File

@@ -57,7 +57,7 @@ import {
processChildContext,
emptyContextObject,
isContextProvider as isLegacyContextProvider,
} from './ReactFiberContext';
} from './ReactFiberLegacyContext';
import {createFiberRoot} from './ReactFiberRoot';
import {isRootDehydrated} from './ReactFiberShellHydration';
import {

View File

@@ -27,10 +27,6 @@ function createCursor<T>(defaultValue: T): StackCursor<T> {
};
}
function isEmpty(): boolean {
return index === -1;
}
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
if (index < 0) {
if (__DEV__) {
@@ -67,31 +63,4 @@ function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
cursor.current = value;
}
function checkThatStackIsEmpty() {
if (__DEV__) {
if (index !== -1) {
console.error(
'Expected an empty stack. Something was not reset properly.',
);
}
}
}
function resetStackAfterFatalErrorInDev() {
if (__DEV__) {
index = -1;
valueStack.length = 0;
fiberStack.length = 0;
}
}
export {
createCursor,
isEmpty,
pop,
push,
// DEV only:
checkThatStackIsEmpty,
resetStackAfterFatalErrorInDev,
};
export {createCursor, pop, push};

View File

@@ -49,7 +49,7 @@ import {
isContextProvider as isLegacyContextProvider,
popContext as popLegacyContext,
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext';
} from './ReactFiberLegacyContext';
import {popProvider} from './ReactFiberNewContext';
import {popCacheProvider} from './ReactFiberCacheComponent';
import {transferActualDuration} from './ReactProfilerTimer';

View File

@@ -357,7 +357,7 @@ import {
flushSyncWorkOnLegacyRootsOnly,
requestTransitionLane,
} from './ReactFiberRootScheduler';
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext';
import {logUncaughtError} from './ReactFiberErrorLogger';
import {
deleteScheduledGesture,

View File

@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -43,12 +44,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
export type Options = {
moduleBaseURL?: string,
callServer?: CallServerCallback,
debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
function createDebugCallbackFromWritableStream(
debugWritable: WritableStream,
): DebugChannelCallback {
const textEncoder = new TextEncoder();
const writer = debugWritable.getWriter();
return message => {
if (message === '') {
writer.close();
} else {
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
// Therefore, we can't report errors from this write back to the Response object.
if (__DEV__) {
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
}
}
};
}
function createResponseFromOptions(options: void | Options) {
return createResponse(
options && options.moduleBaseURL ? options.moduleBaseURL : '',
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ &&
options &&
options.debugChannel !== undefined &&
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
: undefined,
);
}

View File

@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {
@@ -27,6 +29,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -50,6 +54,12 @@ export {
registerClientReference,
} from '../ReactFlightESMReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -86,6 +158,7 @@ function renderToPipeableStream(
moduleBasePath: ClientManifest,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
moduleBasePath,
@@ -95,9 +168,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannel !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannel);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
@@ -126,11 +203,12 @@ function renderToPipeableStream(
},
};
}
function createFakeWritable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -287,8 +366,8 @@ function decodeReply<T>(
export {
renderToPipeableStream,
prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
decodeFormState,
};

View File

@@ -8,7 +8,10 @@
*/
import type {Thenable} from 'shared/ReactTypes.js';
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
import type {
Response as FlightResponse,
DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
@@ -76,6 +79,24 @@ export function createServerReference<A: Iterable<any>, T>(
);
}
function createDebugCallbackFromWritableStream(
debugWritable: WritableStream,
): DebugChannelCallback {
const textEncoder = new TextEncoder();
const writer = debugWritable.getWriter();
return message => {
if (message === '') {
writer.close();
} else {
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
// Therefore, we can't report errors from this write back to the Response object.
if (__DEV__) {
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
}
}
};
}
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
@@ -104,6 +125,7 @@ function startReadingFromStream(
}
export type Options = {
debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
replayConsoleLogs?: boolean,
environmentName?: string,
@@ -128,6 +150,12 @@ export function createFromReadableStream<T>(
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ &&
options &&
options.debugChannel !== undefined &&
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
: undefined,
);
startReadingFromStream(response, stream);
return getRoot(response);
@@ -152,6 +180,12 @@ export function createFromFetch<T>(
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ &&
options &&
options.debugChannel !== undefined &&
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
: undefined,
);
promiseForResponse.then(
function (r) {

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
import {
preloadModule,
@@ -24,6 +27,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -42,12 +47,19 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -57,10 +69,55 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
export function renderToReadableStream(
model: ReactClientValue,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
null,
@@ -70,6 +127,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -83,6 +141,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -117,9 +178,6 @@ export function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
@@ -144,6 +202,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
import {
preloadModule,
@@ -26,6 +29,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -47,12 +52,19 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -62,10 +74,55 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
export function renderToReadableStream(
model: ReactClientValue,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
null,
@@ -75,6 +132,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -88,6 +146,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -122,9 +183,6 @@ export function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
@@ -149,6 +207,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -20,6 +20,8 @@ import type {
ServerReferenceId,
} from '../client/ReactFlightClientConfigBundlerParcel';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -31,6 +33,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -49,6 +53,7 @@ import {
decodeAction as decodeActionImpl,
decodeFormState as decodeFormStateImpl,
} from 'react-server/src/ReactFlightActionServer';
import {
preloadModule,
requireModule,
@@ -60,6 +65,12 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigNode';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -79,7 +90,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -97,6 +170,7 @@ export function renderToPipeableStream(
model: ReactClientValue,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
null,
@@ -106,9 +180,13 @@ export function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannel !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannel);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
@@ -149,7 +227,7 @@ function createFakeWritableFromReadableStreamController(
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
// in web streams there is no backpressure so we can alwas write more
// in web streams there is no backpressure so we can always write more
return true;
},
end() {
@@ -167,13 +245,58 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
export function renderToReadableStream(
model: ReactClientValue,
options?: Options & {
options?: Omit<Options, 'debugChannel'> & {
debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
null,
@@ -183,6 +306,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -196,6 +320,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
let writable: Writable;
const stream = new ReadableStream(
{
@@ -275,6 +402,7 @@ export function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -296,7 +424,6 @@ export function prerenderToNodeStream(
export function prerender(
model: ReactClientValue,
options?: Options & {
signal?: AbortSignal,
},
@@ -338,6 +465,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
export type Options = {
callServer?: CallServerCallback,
debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
function createDebugCallbackFromWritableStream(
debugWritable: WritableStream,
): DebugChannelCallback {
const textEncoder = new TextEncoder();
const writer = debugWritable.getWriter();
return message => {
if (message === '') {
writer.close();
} else {
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
// Therefore, we can't report errors from this write back to the Response object.
if (__DEV__) {
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
}
}
};
}
function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ &&
options &&
options.debugChannel !== undefined &&
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
: undefined,
);
}

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -38,6 +43,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
turbopackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
@@ -143,6 +201,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -43,6 +48,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
turbopackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -121,9 +182,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
@@ -148,6 +206,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -54,6 +58,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigNode';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
turbopackMap: ClientManifest,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
turbopackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannel !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannel);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options & {
options?: Omit<Options, 'debugChannel'> & {
debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
turbopackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
let writable: Writable;
const stream = new ReadableStream(
{
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
export type Options = {
callServer?: CallServerCallback,
debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
function createDebugCallbackFromWritableStream(
debugWritable: WritableStream,
): DebugChannelCallback {
const textEncoder = new TextEncoder();
const writer = debugWritable.getWriter();
return message => {
if (message === '') {
writer.close();
} else {
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
// Therefore, we can't report errors from this write back to the Response object.
if (__DEV__) {
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
}
}
};
}
function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
__DEV__ &&
options &&
options.debugChannel !== undefined &&
options.debugChannel.writable !== undefined
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
: undefined,
);
}

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -38,6 +43,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
webpackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
@@ -143,6 +201,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -7,7 +7,10 @@
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -43,6 +48,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
webpackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
const stream = new ReadableStream(
{
type: 'bytes',
@@ -145,6 +206,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
import type {Duplex} from 'stream';
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
startFlowing,
stopFlowing,
abort,
resolveDebugMessage,
closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -54,6 +58,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
import {
createStringDecoder,
readPartialStringChunk,
readFinalStringChunk,
} from 'react-client/src/ReactFlightClientStreamConfigNode';
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
function startReadingFromDebugChannelReadable(
request: Request,
stream: Readable | WebSocket,
): void {
const stringDecoder = createStringDecoder();
let lastWasPartial = false;
let stringBuffer = '';
function onData(chunk: string | Uint8Array) {
if (typeof chunk === 'string') {
if (lastWasPartial) {
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
lastWasPartial = false;
}
stringBuffer += chunk;
} else {
const buffer: Uint8Array = (chunk: any);
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
lastWasPartial = true;
}
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
}
function onError(error: mixed) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: error,
}),
);
}
function onClose() {
closeDebugChannel(request);
}
if (
// $FlowFixMe[method-unbinding]
typeof stream.addEventListener === 'function' &&
// $FlowFixMe[method-unbinding]
typeof stream.binaryType === 'string'
) {
const ws: WebSocket = (stream: any);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', event => {
// $FlowFixMe
onData(event.data);
});
ws.addEventListener('error', event => {
// $FlowFixMe
onError(event.error);
});
ws.addEventListener('close', onClose);
} else {
const readable: Readable = (stream: any);
readable.on('data', onData);
readable.on('error', onError);
readable.on('end', onClose);
}
}
type Options = {
debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
webpackMap: ClientManifest,
options?: Options,
): PipeableStream {
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
webpackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
if (debugChannel !== undefined) {
startReadingFromDebugChannelReadable(request, debugChannel);
}
return {
pipe<T: Writable>(destination: T): T {
if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
function startReadingFromDebugChannelReadableStream(
request: Request,
stream: ReadableStream,
): void {
const reader = stream.getReader();
const stringDecoder = createStringDecoder();
let stringBuffer = '';
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
const buffer: Uint8Array = (value: any);
stringBuffer += done
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
: readPartialStringChunk(stringDecoder, buffer);
const messages = stringBuffer.split('\n');
for (let i = 0; i < messages.length - 1; i++) {
resolveDebugMessage(request, messages[i]);
}
stringBuffer = messages[messages.length - 1];
if (done) {
closeDebugChannel(request);
return;
}
return reader.read().then(progress).catch(error);
}
function error(e: any) {
abort(
request,
new Error('Lost connection to the Debug Channel.', {
cause: e,
}),
);
}
reader.read().then(progress).catch(error);
}
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options & {
options?: Omit<Options, 'debugChannel'> & {
debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
const debugChannelReadable =
__DEV__ && options && options.debugChannel
? options.debugChannel.readable
: undefined;
const request = createRequest(
model,
webpackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
if (debugChannelReadable !== undefined) {
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
}
let writable: Writable;
const stream = new ReadableStream(
{
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
false,
);
if (options && options.signal) {
const signal = options.signal;

View File

@@ -86,7 +86,7 @@ function describeComponentStackByType(
}
}
if (typeof type.name === 'string') {
return describeDebugInfoFrame(type.name, type.env);
return describeDebugInfoFrame(type.name, type.env, type.debugLocation);
}
}
switch (type) {

View File

@@ -3099,6 +3099,9 @@ function replayElement(
if (task.node === currentNode) {
// This same element suspended so we need to pop the replay we just added.
task.replay = replay;
} else {
// We finished rendering this node, so now we can consume this slot.
replayNodes.splice(i, 1);
}
throw x;
}
@@ -4127,6 +4130,8 @@ function renderNode(
const segment = task.blockedSegment;
if (segment === null) {
// Replay
task = ((task: any): ReplayTask); // Refined
const previousReplaySet: ReplaySet = task.replay;
try {
return renderNodeDestructive(request, task, node, childIndex);
} catch (thrownValue) {
@@ -4166,6 +4171,7 @@ function renderNode(
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
task.componentStack = previousComponentStack;
task.replay = previousReplaySet;
if (__DEV__) {
task.debugTask = previousDebugTask;
}
@@ -4199,6 +4205,7 @@ function renderNode(
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
task.componentStack = previousComponentStack;
task.replay = previousReplaySet;
if (__DEV__) {
task.debugTask = previousDebugTask;
}

View File

@@ -7,7 +7,11 @@
* @flow
*/
import type {ReactDebugInfo, ReactComponentInfo} from 'shared/ReactTypes';
import type {
ReactDebugInfo,
ReactComponentInfo,
ReactStackTrace,
} from 'shared/ReactTypes';
export const IO_NODE = 0;
export const PROMISE_NODE = 1;
@@ -22,10 +26,10 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
export type IONode = {
tag: 0,
owner: null | ReactComponentInfo,
stack: Error, // callsite that spawned the I/O
debugInfo: null, // not used on I/O
stack: ReactStackTrace, // callsite that spawned the I/O
start: number, // start time when the first part of the I/O sequence started
end: number, // we typically don't use this. only when there's no promise intermediate.
promise: null, // not used on I/O
awaited: null, // I/O is only blocked on external.
previous: null | AwaitNode | UnresolvedAwaitNode, // the preceeding await that spawned this new work
};
@@ -33,10 +37,10 @@ export type IONode = {
export type PromiseNode = {
tag: 1,
owner: null | ReactComponentInfo,
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
stack: Error, // callsite that created the Promise
stack: ReactStackTrace, // callsite that created the Promise
start: number, // start time when the Promise was created
end: number, // end time when the Promise was resolved.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
};
@@ -44,10 +48,10 @@ export type PromiseNode = {
export type AwaitNode = {
tag: 2,
owner: null | ReactComponentInfo,
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
start: number, // when we started blocking. This might be later than the I/O started.
end: number, // when we unblocked. This might be later than the I/O resolved if there's CPU time.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
awaited: null | AsyncSequence, // the promise we were waiting on
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
};
@@ -55,10 +59,10 @@ export type AwaitNode = {
export type UnresolvedPromiseNode = {
tag: 3,
owner: null | ReactComponentInfo,
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
stack: Error, // callsite that created the Promise
stack: ReactStackTrace, // callsite that created the Promise
start: number, // start time when the Promise was created
end: -1.1, // set when we resolve.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
};
@@ -66,10 +70,10 @@ export type UnresolvedPromiseNode = {
export type UnresolvedAwaitNode = {
tag: 4,
owner: null | ReactComponentInfo,
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
start: number, // when we started blocking. This might be later than the I/O started.
end: -1.1, // set when we resolve.
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
awaited: null | AsyncSequence, // the promise we were waiting on
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
};

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@ import {
import {resolveOwner} from './flight/ReactFlightCurrentOwner';
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
import {parseStackTrace} from './ReactFlightServerConfig';
// $FlowFixMe[method-unbinding]
const getAsyncId = AsyncResource.prototype.asyncId;
@@ -33,6 +34,23 @@ const getAsyncId = AsyncResource.prototype.asyncId;
const pendingOperations: Map<number, AsyncSequence> =
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
// This is a weird one. This map, keeps a dependent Promise alive if the child Promise is still alive.
// A PromiseNode/AwaitNode cannot hold a strong reference to its own Promise because then it'll never get
// GC:ed. We only need it if a dependent AwaitNode points to it. We could put a reference in the Node
// but that would require a GC pass between every Node that gets destroyed. I.e. the root gets destroy()
// called on it and then that release it from the pendingOperations map which allows the next one to GC
// and so on. By putting this relationship in a WeakMap this could be done as a single pass in the VM.
// We don't actually ever have to read from this map since we have WeakRef reference to these Promises
// if they're still alive. It's also optional information so we could just expose only if GC didn't run.
const awaitedPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
enableAsyncDebugInfo
? new WeakMap()
: (null: any);
const previousPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
enableAsyncDebugInfo
? new WeakMap()
: (null: any);
// Keep the last resolved await as a workaround for async functions missing data.
let lastRanAwait: null | AwaitNode = null;
@@ -44,12 +62,6 @@ function resolvePromiseOrAwaitNode(
resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
? PROMISE_NODE
: AWAIT_NODE): any);
// The Promise can be garbage collected after this so we should extract debugInfo first.
const promise = unresolvedNode.debugInfo.deref();
resolvedNode.debugInfo =
promise === undefined || promise._debugInfo === undefined
? null
: promise._debugInfo;
resolvedNode.end = endTime;
return resolvedNode;
}
@@ -71,6 +83,14 @@ export function initAsyncDebugInfo(): void {
const trigger = pendingOperations.get(triggerAsyncId);
let node: AsyncSequence;
if (type === 'PROMISE') {
if (trigger !== undefined && trigger.promise !== null) {
const triggerPromise = trigger.promise.deref();
if (triggerPromise !== undefined) {
// Keep the awaited Promise alive as long as the child is alive so we can
// trace its value at the end.
awaitedPromise.set(resource, triggerPromise);
}
}
const currentAsyncId = executionAsyncId();
if (currentAsyncId !== triggerAsyncId) {
// When you call .then() on a native Promise, or await/Promise.all() a thenable,
@@ -80,15 +100,23 @@ export function initAsyncDebugInfo(): void {
return;
}
const current = pendingOperations.get(currentAsyncId);
if (current !== undefined && current.promise !== null) {
const currentPromise = current.promise.deref();
if (currentPromise !== undefined) {
// Keep the previous Promise alive as long as the child is alive so we can
// trace its value at the end.
previousPromise.set(resource, currentPromise);
}
}
// If the thing we're waiting on is another Await we still track that sequence
// so that we can later pick the best stack trace in user space.
node = ({
tag: UNRESOLVED_AWAIT_NODE,
owner: resolveOwner(),
debugInfo: new WeakRef((resource: Promise<any>)),
stack: new Error(),
stack: parseStackTrace(new Error(), 5),
start: performance.now(),
end: -1.1, // set when resolved.
promise: new WeakRef((resource: Promise<any>)),
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
previous: current === undefined ? null : current, // The path that led us here.
}: UnresolvedAwaitNode);
@@ -96,10 +124,10 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: UNRESOLVED_PROMISE_NODE,
owner: resolveOwner(),
debugInfo: new WeakRef((resource: Promise<any>)),
stack: new Error(),
stack: parseStackTrace(new Error(), 5),
start: performance.now(),
end: -1.1, // Set when we resolve.
promise: new WeakRef((resource: Promise<any>)),
awaited:
trigger === undefined
? null // It might get overridden when we resolve.
@@ -117,10 +145,10 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: IO_NODE,
owner: resolveOwner(),
debugInfo: null,
stack: new Error(), // This is only used if no native promises are used.
stack: parseStackTrace(new Error(), 3), // This is only used if no native promises are used.
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,
awaited: null,
previous: null,
}: IONode);
@@ -132,10 +160,10 @@ export function initAsyncDebugInfo(): void {
node = ({
tag: IO_NODE,
owner: resolveOwner(),
debugInfo: null,
stack: new Error(),
stack: parseStackTrace(new Error(), 3),
start: performance.now(),
end: -1.1, // Only set when pinged.
promise: null,
awaited: null,
previous: trigger,
}: IONode);

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
import {formatOwnerStack} from './ReactOwnerStackFrames';
let prefix;
let suffix;
export function describeBuiltInComponentFrame(name: string): string {
@@ -38,7 +40,24 @@ export function describeBuiltInComponentFrame(name: string): string {
return '\n' + prefix + name + suffix;
}
export function describeDebugInfoFrame(name: string, env: ?string): string {
export function describeDebugInfoFrame(
name: string,
env: ?string,
location: ?Error,
): string {
if (location != null) {
// If we have a location, it's the child's owner stack. Treat the bottom most frame as
// the location of this function.
const childStack = formatOwnerStack(location);
const idx = childStack.lastIndexOf('\n');
const lastLine = idx === -1 ? childStack : childStack.slice(idx + 1);
if (lastLine.indexOf(name) !== -1) {
// For async stacks it's possible we don't have the owner on it. As a precaution only
// use this frame if it has the name of the function in it.
return '\n' + lastLine;
}
}
return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
}

View File

@@ -51,6 +51,18 @@ function isObjectPrototype(object: any): boolean {
return true;
}
export function isGetter(object: any, name: string): boolean {
const ObjectPrototype = Object.prototype;
if (object === ObjectPrototype || object === null) {
return false;
}
const descriptor = Object.getOwnPropertyDescriptor(object, name);
if (descriptor === undefined) {
return isGetter(getPrototypeOf(object), name);
}
return typeof descriptor.get === 'function';
}
export function isSimpleObject(object: any): boolean {
if (!isObjectPrototype(getPrototypeOf(object))) {
return false;
@@ -80,9 +92,8 @@ export function isSimpleObject(object: any): boolean {
export function objectName(object: mixed): string {
// $FlowFixMe[method-unbinding]
const name = Object.prototype.toString.call(object);
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
return p0;
});
// Extract 'Object' from '[object Object]':
return name.slice(8, name.length - 1);
}
function describeKeyForErrorMessage(key: string): string {

View File

@@ -209,6 +209,7 @@ export type ReactComponentInfo = {
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
+debugStack?: null | Error,
+debugTask?: null | ConsoleTask,
debugLocation?: null | Error,
};
export type ReactEnvironmentInfo = {
@@ -234,6 +235,7 @@ export type ReactIOInfo = {
+name: string, // the name of the async function being called (e.g. "fetch")
+start: number, // the start time
+end: number, // the end time (this might be different from the time the await was unblocked)
+value?: null | Promise<mixed>, // the Promise that was awaited if any, may be rejected
+env?: string, // the environment where this I/O was spawned.
+owner?: null | ReactComponentInfo,
+stack?: null | ReactStackTrace,

View File

@@ -26,3 +26,4 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
export const enableLazyPublicInstanceInFabric = __VARIANT__;
export const renameElementSymbol = __VARIANT__;
export const enableFragmentRefs = __VARIANT__;
export const enableComponentPerformanceTrack = __VARIANT__;

View File

@@ -59,7 +59,6 @@ export const enableProfilerTimer = __PROFILE__;
export const enableReactTestRendererWarning = false;
export const enableRetryLaneExpiration = false;
export const enableSchedulingProfiler = __PROFILE__;
export const enableComponentPerformanceTrack = false;
export const enableScopeAPI = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseCallback = true;
@@ -84,6 +83,8 @@ export const enableSrcObject = false;
export const enableHydrationChangeEvent = true;
export const enableDefaultTransitionIndicator = false;
export const ownerStackLimit = 1e4;
export const enableComponentPerformanceTrack: boolean =
__PROFILE__ && dynamicFlags.enableComponentPerformanceTrack;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@@ -548,5 +548,7 @@
"560": "Cannot use a startGestureTransition() with a comment node root.",
"561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.",
"562": "The render was aborted due to a fatal error.",
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
"564": "Unknown command. The debugChannel was not wired up properly.",
"565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React."
}