Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718105c603 | ||
|
|
82b8f612de |
@@ -44,7 +44,6 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -144,7 +143,6 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const error = new CompilerError();
|
||||
const otherErrors: Array<CompilerErrorDetail> = [];
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
@@ -212,11 +210,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(new CompilerErrorDetail(event.detail));
|
||||
}
|
||||
},
|
||||
logEvent: () => {},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
@@ -243,10 +237,6 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Only include logger errors if there weren't other errors
|
||||
if (!error.hasErrors() && otherErrors.length !== 0) {
|
||||
otherErrors.forEach(e => error.push(e));
|
||||
}
|
||||
if (error.hasErrors()) {
|
||||
return [{kind: 'err', results, error: error}, language];
|
||||
}
|
||||
|
||||
@@ -1355,83 +1355,13 @@ function lowerStatement(
|
||||
|
||||
return;
|
||||
}
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `JavaScript 'with' syntax is not supported`,
|
||||
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration': {
|
||||
/*
|
||||
* We can in theory support nested classes, similarly to functions where we track values
|
||||
* captured by the class and consider mutations of the instances to mutate the class itself
|
||||
*/
|
||||
builder.errors.push({
|
||||
reason: `Support nested class declarations`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'EnumDeclaration':
|
||||
case 'TSEnumDeclaration': {
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'JavaScript `import` and `export` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'TSNamespaceExportDeclaration': {
|
||||
builder.errors.push({
|
||||
reason:
|
||||
'TypeScript `namespace` statements may only appear at the top level of a module',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
case 'TypeAlias':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSTypeAliasDeclaration': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
return;
|
||||
}
|
||||
case 'ClassDeclaration':
|
||||
case 'DeclareClass':
|
||||
case 'DeclareExportAllDeclaration':
|
||||
case 'DeclareExportDeclaration':
|
||||
@@ -1442,14 +1372,31 @@ function lowerStatement(
|
||||
case 'DeclareOpaqueType':
|
||||
case 'DeclareTypeAlias':
|
||||
case 'DeclareVariable':
|
||||
case 'EnumDeclaration':
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'OpaqueType':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSEnumDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration':
|
||||
case 'TSModuleDeclaration':
|
||||
case 'TSTypeAliasDeclaration':
|
||||
case 'TypeAlias': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
case 'TSNamespaceExportDeclaration':
|
||||
case 'WithStatement': {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: stmtPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
loc: stmtPath.node.loc ?? GeneratedSource,
|
||||
node: stmtPath.node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -3555,16 +3502,6 @@ function lowerIdentifier(
|
||||
return place;
|
||||
}
|
||||
default: {
|
||||
if (binding.kind === 'Global' && binding.name === 'eval') {
|
||||
builder.errors.push({
|
||||
reason: `The 'eval' function is not supported`,
|
||||
description:
|
||||
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: exprPath.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
return lowerValueToTemporary(builder, {
|
||||
kind: 'LoadGlobal',
|
||||
binding,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import generate from '@babel/generator';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
@@ -465,7 +466,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
break;
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
value = `UnsupportedNode ${instrValue.node.type}`;
|
||||
value = `UnsupportedNode(${generate(instrValue.node).code})`;
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
|
||||
@@ -829,14 +829,12 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
};
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
const lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
|
||||
}
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: [],
|
||||
};
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected unsupported node`,
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
eval('props.x = true');
|
||||
return <div />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | function Component(props) {
|
||||
> 2 | eval('props.x = true');
|
||||
| ^^^^ InvalidJS: The 'eval' function is not supported. Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler (2:2)
|
||||
3 | return <div />;
|
||||
4 | }
|
||||
5 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
function Component(props) {
|
||||
eval('props.x = true');
|
||||
return <div />;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ let moduleLocal = false;
|
||||
> 3 | var x = [];
|
||||
| ^^^^^^^^^^^ Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (3:3)
|
||||
|
||||
Todo: Support nested class declarations (5:10)
|
||||
Todo: (BuildHIR::lowerStatement) Handle ClassDeclaration statements (5:10)
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement (20:22)
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
function Component(props) {
|
||||
enum Bool {
|
||||
True = 'true',
|
||||
False = 'false',
|
||||
}
|
||||
|
||||
let bool: Bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
return <div>{bool}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
enum Bool {
|
||||
True = "true",
|
||||
False = "false",
|
||||
}
|
||||
|
||||
let bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] !== bool) {
|
||||
t0 = <div>{bool}</div>;
|
||||
$[0] = bool;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Bool is not defined
|
||||
@@ -1,18 +0,0 @@
|
||||
// @flow
|
||||
function Component(props) {
|
||||
enum Bool {
|
||||
True = 'true',
|
||||
False = 'false',
|
||||
}
|
||||
|
||||
let bool: Bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
return <div>{bool}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: true}],
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
enum Bool {
|
||||
True = 'true',
|
||||
False = 'false',
|
||||
}
|
||||
|
||||
let bool: Bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
return <div>{bool}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: true}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Component(props) {
|
||||
const $ = _c(2);
|
||||
enum Bool {
|
||||
True = "true",
|
||||
False = "false",
|
||||
}
|
||||
|
||||
let bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
let t0;
|
||||
if ($[0] !== bool) {
|
||||
t0 = <div>{bool}</div>;
|
||||
$[0] = bool;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: true }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>true</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
function Component(props) {
|
||||
enum Bool {
|
||||
True = 'true',
|
||||
False = 'false',
|
||||
}
|
||||
|
||||
let bool: Bool = Bool.False;
|
||||
if (props.value) {
|
||||
bool = Bool.True;
|
||||
}
|
||||
return <div>{bool}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: true}],
|
||||
};
|
||||
@@ -53,15 +53,6 @@ const React = require('react');
|
||||
const activeDebugChannels =
|
||||
process.env.NODE_ENV === 'development' ? new Map() : null;
|
||||
|
||||
function filterStackFrame(sourceURL, functionName) {
|
||||
return (
|
||||
sourceURL !== '' &&
|
||||
!sourceURL.startsWith('node:') &&
|
||||
!sourceURL.includes('node_modules') &&
|
||||
!sourceURL.endsWith('library.js')
|
||||
);
|
||||
}
|
||||
|
||||
function getDebugChannel(req) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return undefined;
|
||||
@@ -132,7 +123,6 @@ async function renderApp(
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
filterStackFrame,
|
||||
});
|
||||
pipe(res);
|
||||
}
|
||||
@@ -188,9 +178,7 @@ async function prerenderApp(res, returnValue, formState, noCache) {
|
||||
);
|
||||
// For client-invoked server actions we refresh the tree and return a return value.
|
||||
const payload = {root, returnValue, formState};
|
||||
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
|
||||
filterStackFrame,
|
||||
});
|
||||
const {prelude} = await prerenderToNodeStream(payload, moduleMap);
|
||||
prelude.pipe(res);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {GenerateImage} from './GenerateImage.js';
|
||||
import {like, greet, increment} from './actions.js';
|
||||
|
||||
import {getServerState} from './ServerState.js';
|
||||
import {sdkMethod} from './library.js';
|
||||
|
||||
const promisedText = new Promise(resolve =>
|
||||
setTimeout(() => resolve('deferred text'), 50)
|
||||
@@ -121,69 +120,9 @@ async function ServerComponent({noCache}) {
|
||||
return await fetchThirdParty(noCache);
|
||||
}
|
||||
|
||||
let veryDeepObject = [
|
||||
{
|
||||
bar: {
|
||||
baz: {
|
||||
a: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bar: {
|
||||
baz: {
|
||||
a: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bar: {
|
||||
baz: {
|
||||
a: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bar: {
|
||||
baz: {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
g: {
|
||||
h: {
|
||||
i: {
|
||||
j: {
|
||||
k: {
|
||||
l: {
|
||||
m: {
|
||||
yay: 'You reached the end',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default async function App({prerender, noCache}) {
|
||||
const res = await fetch('http://localhost:3001/todos');
|
||||
const todos = await res.json();
|
||||
await sdkMethod('http://localhost:3001/todos');
|
||||
|
||||
console.log('Expand me:', veryDeepObject);
|
||||
|
||||
const dedupedChild = <ServerComponent noCache={noCache} />;
|
||||
const message = getServerState();
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export async function sdkMethod(input, init) {
|
||||
return fetch(input, init).then(async response => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
61
packages/react-client/src/ReactFlightClient.js
vendored
61
packages/react-client/src/ReactFlightClient.js
vendored
@@ -1774,40 +1774,6 @@ function applyConstructor(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function defineLazyGetter<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<T>,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
): any {
|
||||
// We don't immediately initialize it even if it's resolved.
|
||||
// Instead, we wait for the getter to get accessed.
|
||||
Object.defineProperty(parentObject, key, {
|
||||
get: function () {
|
||||
if (chunk.status === RESOLVED_MODEL) {
|
||||
// If it was now resolved, then we initialize it. This may then discover
|
||||
// a new set of lazy references that are then asked for eagerly in case
|
||||
// we get that deep.
|
||||
initializeModelChunk(chunk);
|
||||
}
|
||||
switch (chunk.status) {
|
||||
case INITIALIZED: {
|
||||
return chunk.value;
|
||||
}
|
||||
case ERRORED:
|
||||
throw chunk.reason;
|
||||
}
|
||||
// Otherwise, we didn't have enough time to load the object before it was
|
||||
// accessed or the connection closed. So we just log that it was omitted.
|
||||
// TODO: We should ideally throw here to indicate a difference.
|
||||
return OMITTED_PROP_ERROR;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
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]();
|
||||
@@ -2048,31 +2014,8 @@ function parseModelString(
|
||||
if (value.length > 2) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel) {
|
||||
if (value[2] === '@') {
|
||||
// This is a deferred Promise.
|
||||
const ref = value.slice(3); // We assume this doesn't have a path just id.
|
||||
const id = parseInt(ref, 16);
|
||||
if (!response._chunks.has(id)) {
|
||||
// We haven't seen this id before. Query the server to start sending it.
|
||||
debugChannel('P:' + ref);
|
||||
}
|
||||
// Start waiting. This now creates a pending chunk if it doesn't already exist.
|
||||
// This is the actual Promise we're waiting for.
|
||||
return getChunk(response, id);
|
||||
}
|
||||
const ref = value.slice(2); // We assume this doesn't have a path just id.
|
||||
const id = parseInt(ref, 16);
|
||||
if (!response._chunks.has(id)) {
|
||||
// We haven't seen this id before. Query the server to start sending it.
|
||||
debugChannel('Q:' + ref);
|
||||
}
|
||||
// Start waiting. This now creates a pending chunk if it doesn't already exist.
|
||||
const chunk = getChunk(response, id);
|
||||
if (chunk.status === INITIALIZED) {
|
||||
// We already loaded this before. We can just use the real value.
|
||||
return chunk.value;
|
||||
}
|
||||
return defineLazyGetter(response, chunk, parentObject, key);
|
||||
const ref = value.slice(2);
|
||||
debugChannel('R:' + ref); // Release this reference immediately
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9544,102 +9544,6 @@ describe('ReactDOMFizzServer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('will attempt to render the preamble inline to allow rendering before a later abort in the same task', async () => {
|
||||
const promise = new Promise(() => {});
|
||||
function Pending() {
|
||||
React.use(promise);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
function Abort() {
|
||||
controller.abort();
|
||||
return <Comp />;
|
||||
}
|
||||
|
||||
function Comp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta content="here" />
|
||||
</head>
|
||||
<body>
|
||||
<main>hello</main>
|
||||
<Suspense>
|
||||
<Pending />
|
||||
</Suspense>
|
||||
<Abort />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
const signal = controller.signal;
|
||||
|
||||
let thrownError = null;
|
||||
const errors = [];
|
||||
try {
|
||||
await act(() => {
|
||||
const {pipe, abort} = renderToPipeableStream(<App />, {
|
||||
onError(e, ei) {
|
||||
errors.push({
|
||||
error: e,
|
||||
componentStack: normalizeCodeLocInfo(ei.componentStack),
|
||||
});
|
||||
},
|
||||
});
|
||||
signal.addEventListener('abort', () => abort('boom'));
|
||||
pipe(writable);
|
||||
});
|
||||
} catch (e) {
|
||||
thrownError = e;
|
||||
}
|
||||
|
||||
expect(thrownError).toBe('boom');
|
||||
// TODO there should actually be three errors. One for the pending Suspense, one for the fallback task, and one for the task
|
||||
// that does the abort itself. At the moment abort will flush queues and if there is no pending tasks will close the request before
|
||||
// the task which initiated the abort can even be processed. This is a bug but not one that I am fixing with the current change
|
||||
// so I am asserting the current behavior
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
error: 'boom',
|
||||
componentStack: componentStack([
|
||||
'Pending',
|
||||
'Suspense',
|
||||
'body',
|
||||
'html',
|
||||
'App',
|
||||
]),
|
||||
},
|
||||
{
|
||||
error: 'boom',
|
||||
componentStack: componentStack([
|
||||
'Suspense Fallback',
|
||||
'body',
|
||||
'html',
|
||||
'App',
|
||||
]),
|
||||
// }, {
|
||||
// error: 'boom',
|
||||
// componentStack: componentStack(['Abort', 'body', 'html', 'App'])
|
||||
},
|
||||
]);
|
||||
|
||||
// We expect the render to throw before streaming anything so the default
|
||||
// document is still loaded
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div id="container" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('Will wait to flush Document chunks until all boundaries which might contain a preamble are errored or resolved', async () => {
|
||||
let rejectFirst;
|
||||
const firstPromise = new Promise((_, reject) => {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from 'shared/ReactComponentStackFrame';
|
||||
import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
|
||||
|
||||
function describeFiber(fiber: Fiber, childFiber: null | Fiber): string {
|
||||
function describeFiber(fiber: Fiber): string {
|
||||
switch (fiber.tag) {
|
||||
case HostHoistable:
|
||||
case HostSingleton:
|
||||
@@ -44,10 +44,6 @@ function describeFiber(fiber: Fiber, childFiber: null | Fiber): string {
|
||||
// TODO: When we support Thenables as component types we should rename this.
|
||||
return describeBuiltInComponentFrame('Lazy');
|
||||
case SuspenseComponent:
|
||||
if (fiber.child !== childFiber && childFiber !== null) {
|
||||
// If we came from the second Fiber then we're in the Suspense Fallback.
|
||||
return describeBuiltInComponentFrame('Suspense Fallback');
|
||||
}
|
||||
return describeBuiltInComponentFrame('Suspense');
|
||||
case SuspenseListComponent:
|
||||
return describeBuiltInComponentFrame('SuspenseList');
|
||||
@@ -74,9 +70,8 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
|
||||
try {
|
||||
let info = '';
|
||||
let node: Fiber = workInProgress;
|
||||
let previous: null | Fiber = null;
|
||||
do {
|
||||
info += describeFiber(node, previous);
|
||||
info += describeFiber(node);
|
||||
if (__DEV__) {
|
||||
// Add any Server Component stack frames in reverse order.
|
||||
const debugInfo = node._debugInfo;
|
||||
@@ -93,7 +88,6 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
previous = node;
|
||||
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
||||
node = node.return;
|
||||
} while (node);
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('ReactFragment', () => {
|
||||
function normalizeCodeLocInfo(str) {
|
||||
return (
|
||||
str &&
|
||||
str.replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {
|
||||
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
|
||||
return '\n in ' + name + ' (at **)';
|
||||
})
|
||||
);
|
||||
@@ -168,40 +168,6 @@ describe('ReactFragment', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes built-in for Suspense fallbacks', async () => {
|
||||
const SomethingThatSuspends = React.lazy(() => {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
|
||||
ReactNoop.createRoot({
|
||||
onCaughtError,
|
||||
}).render(
|
||||
<CatchingBoundary>
|
||||
<Suspense fallback={<SomethingThatErrors />}>
|
||||
<SomethingThatSuspends />
|
||||
</Suspense>
|
||||
</CatchingBoundary>,
|
||||
);
|
||||
await waitForAll([]);
|
||||
expect(didCatchErrors).toEqual([
|
||||
'uh oh',
|
||||
componentStack([
|
||||
'SomethingThatErrors',
|
||||
'Suspense Fallback',
|
||||
'CatchingBoundary',
|
||||
]),
|
||||
]);
|
||||
expect(rootCaughtErrors).toEqual([
|
||||
'uh oh',
|
||||
componentStack([
|
||||
'SomethingThatErrors',
|
||||
'Suspense Fallback',
|
||||
'CatchingBoundary',
|
||||
]),
|
||||
__DEV__ ? componentStack(['SomethingThatErrors']) : null,
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableActivity
|
||||
it('includes built-in for Activity', async () => {
|
||||
ReactNoop.createRoot({
|
||||
|
||||
42
packages/react-server/src/ReactFizzServer.js
vendored
42
packages/react-server/src/ReactFizzServer.js
vendored
@@ -2206,7 +2206,7 @@ function renderSuspenseList(
|
||||
|
||||
function renderPreamble(
|
||||
request: Request,
|
||||
task: RenderTask,
|
||||
task: Task,
|
||||
blockedSegment: Segment,
|
||||
node: ReactNodeList,
|
||||
): void {
|
||||
@@ -2219,21 +2219,28 @@ function renderPreamble(
|
||||
false,
|
||||
);
|
||||
blockedSegment.preambleChildren.push(preambleSegment);
|
||||
task.blockedSegment = preambleSegment;
|
||||
try {
|
||||
preambleSegment.status = RENDERING;
|
||||
renderNode(request, task, node, -1);
|
||||
pushSegmentFinale(
|
||||
preambleSegment.chunks,
|
||||
request.renderState,
|
||||
preambleSegment.lastPushedText,
|
||||
preambleSegment.textEmbedded,
|
||||
);
|
||||
preambleSegment.status = COMPLETED;
|
||||
finishedSegment(request, task.blockedBoundary, preambleSegment);
|
||||
} finally {
|
||||
task.blockedSegment = blockedSegment;
|
||||
}
|
||||
// @TODO we can just attempt to render in the current task rather than spawning a new one
|
||||
const preambleTask = createRenderTask(
|
||||
request,
|
||||
null,
|
||||
node,
|
||||
-1,
|
||||
task.blockedBoundary,
|
||||
preambleSegment,
|
||||
task.blockedPreamble,
|
||||
task.hoistableState,
|
||||
request.abortableTasks,
|
||||
task.keyPath,
|
||||
task.formatContext,
|
||||
task.context,
|
||||
task.treeContext,
|
||||
task.row,
|
||||
task.componentStack,
|
||||
!disableLegacyContext ? task.legacyContext : emptyContextObject,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
);
|
||||
pushComponentStack(preambleTask);
|
||||
request.pingedTasks.push(preambleTask);
|
||||
}
|
||||
|
||||
function renderHostElement(
|
||||
@@ -2285,8 +2292,7 @@ function renderHostElement(
|
||||
props,
|
||||
));
|
||||
if (isPreambleContext(newContext)) {
|
||||
// $FlowFixMe: Refined
|
||||
renderPreamble(request, (task: RenderTask), segment, children);
|
||||
renderPreamble(request, task, segment, children);
|
||||
} else {
|
||||
// We use the non-destructive form because if something suspends, we still
|
||||
// need to pop back up and finish this subtree of HTML.
|
||||
|
||||
@@ -26,7 +26,7 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
|
||||
export type IONode = {
|
||||
tag: 0,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | ReactStackTrace, // callsite that spawned the 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
|
||||
@@ -37,7 +37,7 @@ export type IONode = {
|
||||
export type PromiseNode = {
|
||||
tag: 1,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | ReactStackTrace, // 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
|
||||
@@ -48,7 +48,7 @@ export type PromiseNode = {
|
||||
export type AwaitNode = {
|
||||
tag: 2,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | ReactStackTrace, // 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
|
||||
@@ -59,7 +59,7 @@ export type AwaitNode = {
|
||||
export type UnresolvedPromiseNode = {
|
||||
tag: 3,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | ReactStackTrace, // 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
|
||||
@@ -70,7 +70,7 @@ export type UnresolvedPromiseNode = {
|
||||
export type UnresolvedAwaitNode = {
|
||||
tag: 4,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: null | ReactStackTrace, // 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
|
||||
|
||||
203
packages/react-server/src/ReactFlightServer.js
vendored
203
packages/react-server/src/ReactFlightServer.js
vendored
@@ -94,7 +94,6 @@ import {
|
||||
getCurrentAsyncSequence,
|
||||
getAsyncSequenceFromPromise,
|
||||
parseStackTrace,
|
||||
parseStackTracePrivate,
|
||||
supportsComponentStorage,
|
||||
componentStorage,
|
||||
unbadgeConsole,
|
||||
@@ -252,54 +251,6 @@ function filterStackTrace(
|
||||
return filteredStack;
|
||||
}
|
||||
|
||||
function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
|
||||
const filterStackFrame = request.filterStackFrame;
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const callsite = stack[i];
|
||||
const functionName = callsite[0];
|
||||
const url = devirtualizeURL(callsite[1]);
|
||||
const lineNumber = callsite[2];
|
||||
const columnNumber = callsite[3];
|
||||
// Ignore async stack frames because they're not "real". We'd expect to have at least
|
||||
// one non-async frame if we're actually executing inside a first party function.
|
||||
// Otherwise we might just be in the resume of a third party function that resumed
|
||||
// inside a first party stack.
|
||||
const isAsync = callsite[6];
|
||||
if (
|
||||
!isAsync &&
|
||||
filterStackFrame(url, functionName, lineNumber, columnNumber)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isAwaitInUserspace(
|
||||
request: Request,
|
||||
stack: ReactStackTrace,
|
||||
): boolean {
|
||||
let firstFrame = 0;
|
||||
while (stack.length > firstFrame && stack[firstFrame][0] === 'Promise.then') {
|
||||
// Skip Promise.then frame itself.
|
||||
firstFrame++;
|
||||
}
|
||||
if (stack.length > firstFrame) {
|
||||
// Check if the very first stack frame that awaited this Promise was in user space.
|
||||
// TODO: This doesn't take into account wrapper functions such as our fake .then()
|
||||
// in FlightClient which will always be considered third party awaits if you call
|
||||
// .then directly.
|
||||
const filterStackFrame = request.filterStackFrame;
|
||||
const callsite = stack[firstFrame];
|
||||
const functionName = callsite[0];
|
||||
const url = devirtualizeURL(callsite[1]);
|
||||
const lineNumber = callsite[2];
|
||||
const columnNumber = callsite[3];
|
||||
return filterStackFrame(url, functionName, lineNumber, columnNumber);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
initAsyncDebugInfo();
|
||||
|
||||
function patchConsole(consoleInst: typeof console, methodName: string) {
|
||||
@@ -325,7 +276,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
|
||||
// one stack frame but keeping it simple for now and include all frames.
|
||||
const stack = filterStackTrace(
|
||||
request,
|
||||
parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
|
||||
parseStackTrace(new Error('react-stack-top-frame'), 1),
|
||||
);
|
||||
request.pendingDebugChunks++;
|
||||
const owner: null | ReactComponentInfo = resolveOwner();
|
||||
@@ -801,19 +752,6 @@ function serializeDebugThenable(
|
||||
return ref;
|
||||
}
|
||||
|
||||
const deferredDebugObjects = request.deferredDebugObjects;
|
||||
if (deferredDebugObjects !== null) {
|
||||
// For Promises that are not yet resolved, we always defer them. They are async anyway so it's
|
||||
// safe to defer them. This also ensures that we don't eagerly call .then() on a Promise that
|
||||
// otherwise wouldn't have initialized. It also ensures that we don't "handle" a rejection
|
||||
// that otherwise would have triggered unhandled rejection.
|
||||
deferredDebugObjects.retained.set(id, (thenable: any));
|
||||
const deferredRef = '$Y@' + id.toString(16);
|
||||
// We can now refer to the deferred object in the future.
|
||||
request.writtenDebugObjects.set(thenable, deferredRef);
|
||||
return deferredRef;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
thenable.then(
|
||||
@@ -866,36 +804,6 @@ function serializeDebugThenable(
|
||||
return ref;
|
||||
}
|
||||
|
||||
function emitRequestedDebugThenable(
|
||||
request: Request,
|
||||
id: number,
|
||||
counter: {objectLimit: number},
|
||||
thenable: Thenable<any>,
|
||||
): void {
|
||||
thenable.then(
|
||||
value => {
|
||||
if (request.status === ABORTING) {
|
||||
emitDebugHaltChunk(request, id);
|
||||
enqueueFlush(request);
|
||||
return;
|
||||
}
|
||||
emitOutlinedDebugModelChunk(request, id, counter, value);
|
||||
enqueueFlush(request);
|
||||
},
|
||||
reason => {
|
||||
if (request.status === ABORTING) {
|
||||
emitDebugHaltChunk(request, id);
|
||||
enqueueFlush(request);
|
||||
return;
|
||||
}
|
||||
// We don't log these errors since they didn't actually throw into Flight.
|
||||
const digest = '';
|
||||
emitErrorChunk(request, id, digest, reason, true);
|
||||
enqueueFlush(request);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function serializeThenable(
|
||||
request: Request,
|
||||
task: Task,
|
||||
@@ -1079,9 +987,8 @@ function serializeReadableStream(
|
||||
signal.removeEventListener('abort', abortStream);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.abortableTasks.delete(streamTask);
|
||||
haltTask(streamTask, request);
|
||||
finishHaltedTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, reason);
|
||||
@@ -1209,9 +1116,8 @@ function serializeAsyncIterable(
|
||||
signal.removeEventListener('abort', abortIterable);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.abortableTasks.delete(streamTask);
|
||||
haltTask(streamTask, request);
|
||||
finishHaltedTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, signal.reason);
|
||||
@@ -2182,10 +2088,7 @@ function visitAsyncNode(
|
||||
// If the ioNode was a Promise, then that means we found one in user space since otherwise
|
||||
// we would've returned an IO node. We assume this has the best stack.
|
||||
match = ioNode;
|
||||
} else if (
|
||||
node.stack === null ||
|
||||
!hasUnfilteredFrame(request, node.stack)
|
||||
) {
|
||||
} else if (filterStackTrace(request, node.stack).length === 0) {
|
||||
// If this Promise was created inside only third party code, then try to use
|
||||
// the inner I/O node instead. This could happen if third party calls into first
|
||||
// party to perform some I/O.
|
||||
@@ -2198,10 +2101,7 @@ function visitAsyncNode(
|
||||
// We aborted this render. If this Promise spanned the abort time it was probably the
|
||||
// Promise that was aborted. This won't necessarily have I/O associated with it but
|
||||
// it's a point of interest.
|
||||
if (
|
||||
node.stack !== null &&
|
||||
hasUnfilteredFrame(request, node.stack)
|
||||
) {
|
||||
if (filterStackTrace(request, node.stack).length > 0) {
|
||||
match = node;
|
||||
}
|
||||
}
|
||||
@@ -2247,10 +2147,35 @@ function visitAsyncNode(
|
||||
// just part of a previous component's rendering.
|
||||
match = ioNode;
|
||||
} else {
|
||||
if (
|
||||
node.stack === null ||
|
||||
!isAwaitInUserspace(request, node.stack)
|
||||
let isAwaitInUserspace = false;
|
||||
const fullStack = node.stack;
|
||||
let firstFrame = 0;
|
||||
while (
|
||||
fullStack.length > firstFrame &&
|
||||
fullStack[firstFrame][0] === 'Promise.then'
|
||||
) {
|
||||
// Skip Promise.then frame itself.
|
||||
firstFrame++;
|
||||
}
|
||||
if (fullStack.length > firstFrame) {
|
||||
// Check if the very first stack frame that awaited this Promise was in user space.
|
||||
// TODO: This doesn't take into account wrapper functions such as our fake .then()
|
||||
// in FlightClient which will always be considered third party awaits if you call
|
||||
// .then directly.
|
||||
const filterStackFrame = request.filterStackFrame;
|
||||
const callsite = fullStack[firstFrame];
|
||||
const functionName = callsite[0];
|
||||
const url = devirtualizeURL(callsite[1]);
|
||||
const lineNumber = callsite[2];
|
||||
const columnNumber = callsite[3];
|
||||
isAwaitInUserspace = filterStackFrame(
|
||||
url,
|
||||
functionName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
);
|
||||
}
|
||||
if (!isAwaitInUserspace) {
|
||||
// If this await was fully filtered out, then it was inside third party code
|
||||
// such as in an external library. We return the I/O node and try another await.
|
||||
match = ioNode;
|
||||
@@ -2279,10 +2204,7 @@ function visitAsyncNode(
|
||||
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
|
||||
env: env,
|
||||
owner: node.owner,
|
||||
stack:
|
||||
node.stack === null
|
||||
? null
|
||||
: filterStackTrace(request, node.stack),
|
||||
stack: filterStackTrace(request, node.stack),
|
||||
});
|
||||
// Mark the end time of the await. If we're aborting then we don't emit this
|
||||
// to signal that this never resolved inside this render.
|
||||
@@ -2970,9 +2892,7 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
signal.removeEventListener('abort', abortBlob);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
request.abortableTasks.delete(newTask);
|
||||
haltTask(newTask, request);
|
||||
finishHaltedTask(newTask, request);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, newTask, reason);
|
||||
@@ -4128,6 +4048,10 @@ function emitIOInfoChunk(
|
||||
start: relativeStartTimestamp,
|
||||
end: relativeEndTimestamp,
|
||||
};
|
||||
if (value !== undefined) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugIOInfo.value = value;
|
||||
}
|
||||
if (env != null) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugIOInfo.env = env;
|
||||
@@ -4140,10 +4064,6 @@ function emitIOInfoChunk(
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugIOInfo.owner = owner;
|
||||
}
|
||||
if (value !== undefined) {
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugIOInfo.value = value;
|
||||
}
|
||||
const json: string = serializeDebugModel(request, objectLimit, debugIOInfo);
|
||||
const row = id.toString(16) + ':J' + json + '\n';
|
||||
const processedChunk = stringToChunk(row);
|
||||
@@ -4401,12 +4321,6 @@ function renderDebugModel(
|
||||
if (parentReference !== undefined) {
|
||||
// If the parent has a reference, we can refer to this object indirectly
|
||||
// through the property name inside that parent.
|
||||
if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
|
||||
// If we are going to defer this, don't dedupe it since then we'd dedupe it to be
|
||||
// deferred in future reference.
|
||||
return serializeDeferredObject(request, value);
|
||||
}
|
||||
|
||||
let propertyName = parentPropertyName;
|
||||
if (isArray(parent) && parent[0] === REACT_ELEMENT_TYPE) {
|
||||
// For elements, we've converted it to an array but we'll have converted
|
||||
@@ -4431,15 +4345,8 @@ function renderDebugModel(
|
||||
} else if (debugNoOutline !== value) {
|
||||
// If this isn't the root object (like meta data) and we don't have an id for it, outline
|
||||
// it so that we can dedupe it by reference later.
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (typeof value.then === 'function') {
|
||||
// If this is a Promise we're going to assign it an external ID anyway which can be deduped.
|
||||
const thenable: Thenable<any> = (value: any);
|
||||
return serializeDebugThenable(request, counter, thenable);
|
||||
} else {
|
||||
const outlinedId = outlineDebugModel(request, counter, value);
|
||||
return serializeByValueID(outlinedId);
|
||||
}
|
||||
const outlinedId = outlineDebugModel(request, counter, value);
|
||||
return serializeByValueID(outlinedId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4889,15 +4796,10 @@ function emitConsoleChunk(
|
||||
const payload = [methodName, stackTrace, owner, env];
|
||||
// $FlowFixMe[method-unbinding]
|
||||
payload.push.apply(payload, args);
|
||||
const objectLimit = request.deferredDebugObjects === null ? 500 : 10;
|
||||
let json = serializeDebugModel(
|
||||
request,
|
||||
objectLimit + stackTrace.length,
|
||||
payload,
|
||||
);
|
||||
let json = serializeDebugModel(request, 500, payload);
|
||||
if (json[0] !== '[') {
|
||||
// This looks like an error. Try a simpler object.
|
||||
json = serializeDebugModel(request, 10 + stackTrace.length, [
|
||||
json = serializeDebugModel(request, 500, [
|
||||
methodName,
|
||||
stackTrace,
|
||||
owner,
|
||||
@@ -5834,32 +5736,11 @@ export function resolveDebugMessage(request: Request, message: string): void {
|
||||
if (retainedValue !== undefined) {
|
||||
// If we still have this object, and haven't emitted it before, emit it on the stream.
|
||||
const counter = {objectLimit: 10};
|
||||
deferredDebugObjects.retained.delete(id);
|
||||
deferredDebugObjects.existing.delete(retainedValue);
|
||||
emitOutlinedDebugModelChunk(request, id, counter, retainedValue);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 80 /* "P" */:
|
||||
// Query Promise IDs
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
const retainedValue = deferredDebugObjects.retained.get(id);
|
||||
if (retainedValue !== undefined) {
|
||||
// If we still have this Promise, and haven't emitted it before, wait for it
|
||||
// and then emit it on the stream.
|
||||
const counter = {objectLimit: 10};
|
||||
deferredDebugObjects.retained.delete(id);
|
||||
emitRequestedDebugThenable(
|
||||
request,
|
||||
id,
|
||||
counter,
|
||||
(retainedValue: any),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
'Unknown command. The debugChannel was not wired up properly.',
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactStackTrace} from 'shared/ReactTypes';
|
||||
|
||||
import type {
|
||||
AsyncSequence,
|
||||
IONode,
|
||||
@@ -26,10 +24,9 @@ import {
|
||||
UNRESOLVED_AWAIT_NODE,
|
||||
} from './ReactFlightAsyncSequence';
|
||||
import {resolveOwner} from './flight/ReactFlightCurrentOwner';
|
||||
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
|
||||
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
|
||||
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
||||
import {parseStackTracePrivate} from './ReactFlightServerConfig';
|
||||
import {parseStackTrace} from './ReactFlightServerConfig';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const getAsyncId = AsyncResource.prototype.asyncId;
|
||||
@@ -37,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;
|
||||
|
||||
@@ -52,8 +66,6 @@ function resolvePromiseOrAwaitNode(
|
||||
return resolvedNode;
|
||||
}
|
||||
|
||||
const emptyStack: ReactStackTrace = [];
|
||||
|
||||
// Initialize the tracing of async operations.
|
||||
// We do this globally since the async work can potentially eagerly
|
||||
// start before the first request and once requests start they can interleave.
|
||||
@@ -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,
|
||||
@@ -79,58 +99,32 @@ export function initAsyncDebugInfo(): void {
|
||||
// We don't track awaits on things that started outside our tracked scope.
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
let stack = null;
|
||||
let promiseRef: WeakRef<Promise<any>>;
|
||||
if (
|
||||
trigger.stack !== null &&
|
||||
(trigger.tag === AWAIT_NODE ||
|
||||
trigger.tag === UNRESOLVED_AWAIT_NODE)
|
||||
) {
|
||||
// We already had a stack for an await. In a chain of awaits we'll only need one good stack.
|
||||
// We mark it with an empty stack to signal to any await on this await that we have a stack.
|
||||
stack = emptyStack;
|
||||
if (resource._debugInfo !== undefined) {
|
||||
// We may need to forward this debug info at the end so we need to retain this promise.
|
||||
promiseRef = new WeakRef((resource: Promise<any>));
|
||||
} else {
|
||||
// Otherwise, we can just refer to the inner one since that's the one we'll log anyway.
|
||||
promiseRef = trigger.promise;
|
||||
}
|
||||
} else {
|
||||
promiseRef = new WeakRef((resource: Promise<any>));
|
||||
const request = resolveRequest();
|
||||
if (request === null) {
|
||||
// We don't collect stacks for awaits that weren't in the scope of a specific render.
|
||||
} else {
|
||||
stack = parseStackTracePrivate(new Error(), 5);
|
||||
if (stack !== null && !isAwaitInUserspace(request, stack)) {
|
||||
// If this await was not done directly in user space, then clear the stack. We won't use it
|
||||
// anyway. This lets future awaits on this await know that we still need to get their stacks
|
||||
// until we find one in user space.
|
||||
stack = null;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
const current = pendingOperations.get(currentAsyncId);
|
||||
// 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(),
|
||||
stack: stack,
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // set when resolved.
|
||||
promise: promiseRef,
|
||||
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);
|
||||
} else {
|
||||
const owner = resolveOwner();
|
||||
node = ({
|
||||
tag: UNRESOLVED_PROMISE_NODE,
|
||||
owner: owner,
|
||||
stack:
|
||||
owner === null ? null : parseStackTracePrivate(new Error(), 5),
|
||||
owner: resolveOwner(),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Set when we resolve.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
@@ -148,12 +142,10 @@ export function initAsyncDebugInfo(): void {
|
||||
) {
|
||||
if (trigger === undefined) {
|
||||
// We have begun a new I/O sequence.
|
||||
const owner = resolveOwner();
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: owner,
|
||||
stack:
|
||||
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
|
||||
owner: resolveOwner(),
|
||||
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,
|
||||
@@ -165,12 +157,10 @@ export function initAsyncDebugInfo(): void {
|
||||
trigger.tag === UNRESOLVED_AWAIT_NODE
|
||||
) {
|
||||
// We have begun a new I/O sequence after the await.
|
||||
const owner = resolveOwner();
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: owner,
|
||||
stack:
|
||||
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
|
||||
owner: resolveOwner(),
|
||||
stack: parseStackTrace(new Error(), 3),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
|
||||
@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectStackTracePrivate(
|
||||
function collectStackTrace(
|
||||
error: Error,
|
||||
structuredStackTrace: CallSite[],
|
||||
): string {
|
||||
@@ -63,9 +63,7 @@ function collectStackTracePrivate(
|
||||
// Skip everything after the bottom frame since it'll be internals.
|
||||
break;
|
||||
} else if (callSite.isNative()) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
const isAsync = callSite.isAsync();
|
||||
result.push([name, '', 0, 0, 0, 0, isAsync]);
|
||||
result.push([name, '', 0, 0, 0, 0]);
|
||||
} else {
|
||||
// We encode complex function calls as if they're part of the function
|
||||
// name since we cannot simulate the complex ones and they look the same
|
||||
@@ -81,11 +79,11 @@ function collectStackTracePrivate(
|
||||
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
|
||||
if (filename === '<anonymous>') {
|
||||
filename = '';
|
||||
if (callSite.isEval()) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
if (callSite.isEval() && !filename) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
const line = callSite.getLineNumber() || 0;
|
||||
@@ -100,28 +98,9 @@ function collectStackTracePrivate(
|
||||
typeof callSite.getEnclosingColumnNumber === 'function'
|
||||
? (callSite: any).getEnclosingColumnNumber() || 0
|
||||
: 0;
|
||||
// $FlowFixMe[prop-missing]
|
||||
const isAsync = callSite.isAsync();
|
||||
result.push([
|
||||
name,
|
||||
filename,
|
||||
line,
|
||||
col,
|
||||
enclosingLine,
|
||||
enclosingCol,
|
||||
isAsync,
|
||||
]);
|
||||
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
|
||||
}
|
||||
}
|
||||
collectedStackTrace = result;
|
||||
return '';
|
||||
}
|
||||
|
||||
function collectStackTrace(
|
||||
error: Error,
|
||||
structuredStackTrace: CallSite[],
|
||||
): string {
|
||||
collectStackTracePrivate(error, structuredStackTrace);
|
||||
// At the same time we generate a string stack trace just in case someone
|
||||
// else reads it. Ideally, we'd call the previous prepareStackTrace to
|
||||
// ensure it's in the expected format but it's common for that to be
|
||||
@@ -136,6 +115,7 @@ function collectStackTrace(
|
||||
for (let i = 0; i < structuredStackTrace.length; i++) {
|
||||
stack += '\n at ' + structuredStackTrace[i].toString();
|
||||
}
|
||||
collectedStackTrace = result;
|
||||
return stack;
|
||||
}
|
||||
|
||||
@@ -151,26 +131,6 @@ const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
|
||||
? new WeakMap()
|
||||
: (null: any);
|
||||
|
||||
// This version is only used when React fully owns the Error object and there's no risk of it having
|
||||
// been already initialized and no risky that anyone else will initialize it later.
|
||||
export function parseStackTracePrivate(
|
||||
error: Error,
|
||||
skipFrames: number,
|
||||
): null | ReactStackTrace {
|
||||
collectedStackTrace = null;
|
||||
framesToSkip = skipFrames;
|
||||
const previousPrepare = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = collectStackTracePrivate;
|
||||
try {
|
||||
if (error.stack !== '') {
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
Error.prepareStackTrace = previousPrepare;
|
||||
}
|
||||
return collectedStackTrace;
|
||||
}
|
||||
|
||||
export function parseStackTrace(
|
||||
error: Error,
|
||||
skipFrames: number,
|
||||
@@ -233,12 +193,8 @@ export function parseStackTrace(
|
||||
continue;
|
||||
}
|
||||
let name = parsed[1] || '';
|
||||
let isAsync = parsed[8] === 'async ';
|
||||
if (name === '<anonymous>') {
|
||||
name = '';
|
||||
} else if (name.startsWith('async ')) {
|
||||
name = name.slice(5);
|
||||
isAsync = true;
|
||||
}
|
||||
let filename = parsed[2] || parsed[5] || '';
|
||||
if (filename === '<anonymous>') {
|
||||
@@ -246,7 +202,7 @@ export function parseStackTrace(
|
||||
}
|
||||
const line = +(parsed[3] || parsed[6]);
|
||||
const col = +(parsed[4] || parsed[7]);
|
||||
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
|
||||
parsedFrames.push([name, filename, line, col, 0, 0]);
|
||||
}
|
||||
stackTraceCache.set(error, parsedFrames);
|
||||
return parsedFrames;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -188,7 +188,6 @@ export type ReactCallSite = [
|
||||
number, // column number
|
||||
number, // enclosing line number
|
||||
number, // enclosing column number
|
||||
boolean, // async resume
|
||||
];
|
||||
|
||||
export type ReactStackTrace = Array<ReactCallSite>;
|
||||
|
||||
Reference in New Issue
Block a user