Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a7e8557da | ||
|
|
4a3ff8eed6 | ||
|
|
ec4374c387 | ||
|
|
60b5271a9a | ||
|
|
033edca721 | ||
|
|
e6dc25daea | ||
|
|
150f022444 | ||
|
|
49ded1d12a | ||
|
|
3a43e72d66 | ||
|
|
8ba3501cd9 |
@@ -44,6 +44,7 @@ import {
|
||||
PrintedCompilerPipelineValue,
|
||||
} from './Output';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
|
||||
|
||||
function parseInput(
|
||||
input: string,
|
||||
@@ -143,6 +144,7 @@ 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)) {
|
||||
@@ -210,7 +212,11 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
},
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
logEvent: (_filename: string | null, event: LoggerEvent) => {
|
||||
if (event.kind === 'CompileError') {
|
||||
otherErrors.push(new CompilerErrorDetail(event.detail));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
@@ -237,6 +243,10 @@ 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,13 +1355,48 @@ function lowerStatement(
|
||||
|
||||
return;
|
||||
}
|
||||
case 'TypeAlias':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSTypeAliasDeclaration': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
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 'ClassDeclaration':
|
||||
case 'DeclareClass':
|
||||
case 'DeclareExportAllDeclaration':
|
||||
case 'DeclareExportDeclaration':
|
||||
@@ -1372,20 +1407,23 @@ function lowerStatement(
|
||||
case 'DeclareOpaqueType':
|
||||
case 'DeclareTypeAlias':
|
||||
case 'DeclareVariable':
|
||||
case 'EnumDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'OpaqueType':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSTypeAliasDeclaration':
|
||||
case 'TypeAlias': {
|
||||
// We do not preserve type annotations/syntax through transformation
|
||||
return;
|
||||
}
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ImportDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'OpaqueType':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSEnumDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSImportEqualsDeclaration':
|
||||
case 'TSModuleDeclaration':
|
||||
case 'TSNamespaceExportDeclaration':
|
||||
case 'WithStatement': {
|
||||
case 'TSNamespaceExportDeclaration': {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
|
||||
severity: ErrorSeverity.Todo,
|
||||
@@ -3502,6 +3540,16 @@ 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,7 +5,6 @@
|
||||
* 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';
|
||||
@@ -466,7 +465,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
break;
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
value = `UnsupportedNode(${generate(instrValue.node).code})`;
|
||||
value = `UnsupportedNode ${instrValue.node.type}`;
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
|
||||
@@ -829,12 +829,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
|
||||
};
|
||||
}
|
||||
case 'UnsupportedNode': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected unsupported node`,
|
||||
description: null,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
const lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Never});
|
||||
}
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: [],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
## 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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
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: (BuildHIR::lowerStatement) Handle ClassDeclaration statements (5:10)
|
||||
Todo: Support nested class declarations (5:10)
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement (20:22)
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,18 @@
|
||||
// @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}],
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,17 @@
|
||||
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,6 +53,15 @@ 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;
|
||||
@@ -123,6 +132,7 @@ async function renderApp(
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
filterStackFrame,
|
||||
});
|
||||
pipe(res);
|
||||
}
|
||||
@@ -178,7 +188,9 @@ 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);
|
||||
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
|
||||
filterStackFrame,
|
||||
});
|
||||
prelude.pipe(res);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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)
|
||||
@@ -180,6 +181,7 @@ let veryDeepObject = [
|
||||
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);
|
||||
|
||||
|
||||
9
fixtures/flight/src/library.js
Normal file
9
fixtures/flight/src/library.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export async function sdkMethod(input, init) {
|
||||
return fetch(input, init).then(async response => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
12
packages/react-client/src/ReactFlightClient.js
vendored
12
packages/react-client/src/ReactFlightClient.js
vendored
@@ -2048,6 +2048,18 @@ 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)) {
|
||||
|
||||
100
packages/react-server/src/ReactFlightServer.js
vendored
100
packages/react-server/src/ReactFlightServer.js
vendored
@@ -94,6 +94,7 @@ import {
|
||||
getCurrentAsyncSequence,
|
||||
getAsyncSequenceFromPromise,
|
||||
parseStackTrace,
|
||||
parseStackTracePrivate,
|
||||
supportsComponentStorage,
|
||||
componentStorage,
|
||||
unbadgeConsole,
|
||||
@@ -259,7 +260,15 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
|
||||
const url = devirtualizeURL(callsite[1]);
|
||||
const lineNumber = callsite[2];
|
||||
const columnNumber = callsite[3];
|
||||
if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -316,7 +325,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,
|
||||
parseStackTrace(new Error('react-stack-top-frame'), 1),
|
||||
parseStackTracePrivate(new Error('react-stack-top-frame'), 1) || [],
|
||||
);
|
||||
request.pendingDebugChunks++;
|
||||
const owner: null | ReactComponentInfo = resolveOwner();
|
||||
@@ -792,6 +801,19 @@ 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(
|
||||
@@ -844,6 +866,36 @@ 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,
|
||||
@@ -1027,8 +1079,9 @@ function serializeReadableStream(
|
||||
signal.removeEventListener('abort', abortStream);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
haltTask(streamTask, request);
|
||||
finishHaltedTask(streamTask, request);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, reason);
|
||||
@@ -1156,8 +1209,9 @@ function serializeAsyncIterable(
|
||||
signal.removeEventListener('abort', abortIterable);
|
||||
const reason = signal.reason;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
haltTask(streamTask, request);
|
||||
finishHaltedTask(streamTask, request);
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, signal.reason);
|
||||
@@ -2916,7 +2970,9 @@ 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);
|
||||
@@ -4345,6 +4401,12 @@ 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
|
||||
@@ -4369,8 +4431,15 @@ 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.
|
||||
const outlinedId = outlineDebugModel(request, counter, value);
|
||||
return serializeByValueID(outlinedId);
|
||||
// $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5772,6 +5841,25 @@ export function resolveDebugMessage(request: Request, message: string): void {
|
||||
}
|
||||
}
|
||||
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.',
|
||||
|
||||
@@ -29,7 +29,7 @@ import {resolveOwner} from './flight/ReactFlightCurrentOwner';
|
||||
import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
|
||||
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
|
||||
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
||||
import {parseStackTrace} from './ReactFlightServerConfig';
|
||||
import {parseStackTracePrivate} from './ReactFlightServerConfig';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const getAsyncId = AsyncResource.prototype.asyncId;
|
||||
@@ -37,23 +37,6 @@ 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;
|
||||
|
||||
@@ -88,14 +71,6 @@ 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,
|
||||
@@ -104,18 +79,10 @@ export function initAsyncDebugInfo(): void {
|
||||
// We don't track awaits on things that started outside our tracked scope.
|
||||
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.
|
||||
let stack = null;
|
||||
let promiseRef: WeakRef<Promise<any>>;
|
||||
if (
|
||||
trigger.stack !== null &&
|
||||
(trigger.tag === AWAIT_NODE ||
|
||||
@@ -124,13 +91,21 @@ export function initAsyncDebugInfo(): void {
|
||||
// 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 = parseStackTrace(new Error(), 5);
|
||||
if (!isAwaitInUserspace(request, stack)) {
|
||||
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.
|
||||
@@ -138,13 +113,14 @@ export function initAsyncDebugInfo(): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
const current = pendingOperations.get(currentAsyncId);
|
||||
node = ({
|
||||
tag: UNRESOLVED_AWAIT_NODE,
|
||||
owner: resolveOwner(),
|
||||
stack: stack,
|
||||
start: performance.now(),
|
||||
end: -1.1, // set when resolved.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
promise: promiseRef,
|
||||
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);
|
||||
@@ -153,7 +129,8 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: UNRESOLVED_PROMISE_NODE,
|
||||
owner: owner,
|
||||
stack: owner === null ? null : parseStackTrace(new Error(), 5),
|
||||
stack:
|
||||
owner === null ? null : parseStackTracePrivate(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Set when we resolve.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
@@ -175,7 +152,8 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: owner,
|
||||
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
|
||||
stack:
|
||||
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
@@ -191,7 +169,8 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: owner,
|
||||
stack: owner === null ? parseStackTrace(new Error(), 3) : null,
|
||||
stack:
|
||||
owner === null ? parseStackTracePrivate(new Error(), 3) : null,
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
|
||||
@@ -49,7 +49,7 @@ function getMethodCallName(callSite: CallSite): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectStackTrace(
|
||||
function collectStackTracePrivate(
|
||||
error: Error,
|
||||
structuredStackTrace: CallSite[],
|
||||
): string {
|
||||
@@ -63,7 +63,9 @@ function collectStackTrace(
|
||||
// Skip everything after the bottom frame since it'll be internals.
|
||||
break;
|
||||
} else if (callSite.isNative()) {
|
||||
result.push([name, '', 0, 0, 0, 0]);
|
||||
// $FlowFixMe[prop-missing]
|
||||
const isAsync = callSite.isAsync();
|
||||
result.push([name, '', 0, 0, 0, 0, isAsync]);
|
||||
} 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
|
||||
@@ -79,11 +81,11 @@ function collectStackTrace(
|
||||
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
|
||||
if (filename === '<anonymous>') {
|
||||
filename = '';
|
||||
}
|
||||
if (callSite.isEval() && !filename) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
if (callSite.isEval()) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
}
|
||||
const line = callSite.getLineNumber() || 0;
|
||||
@@ -98,9 +100,28 @@ function collectStackTrace(
|
||||
typeof callSite.getEnclosingColumnNumber === 'function'
|
||||
? (callSite: any).getEnclosingColumnNumber() || 0
|
||||
: 0;
|
||||
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
|
||||
// $FlowFixMe[prop-missing]
|
||||
const isAsync = callSite.isAsync();
|
||||
result.push([
|
||||
name,
|
||||
filename,
|
||||
line,
|
||||
col,
|
||||
enclosingLine,
|
||||
enclosingCol,
|
||||
isAsync,
|
||||
]);
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -115,7 +136,6 @@ function collectStackTrace(
|
||||
for (let i = 0; i < structuredStackTrace.length; i++) {
|
||||
stack += '\n at ' + structuredStackTrace[i].toString();
|
||||
}
|
||||
collectedStackTrace = result;
|
||||
return stack;
|
||||
}
|
||||
|
||||
@@ -131,6 +151,26 @@ 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,
|
||||
@@ -193,8 +233,12 @@ 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>') {
|
||||
@@ -202,7 +246,7 @@ export function parseStackTrace(
|
||||
}
|
||||
const line = +(parsed[3] || parsed[6]);
|
||||
const col = +(parsed[4] || parsed[7]);
|
||||
parsedFrames.push([name, filename, line, col, 0, 0]);
|
||||
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
|
||||
}
|
||||
stackTraceCache.set(error, parsedFrames);
|
||||
return parsedFrames;
|
||||
|
||||
@@ -2515,4 +2515,237 @@ describe('ReactFlightAsyncDebugInfo', () => {
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
it('can track IO in third-party code', async () => {
|
||||
async function thirdParty(endpoint) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve('third-party ' + endpoint);
|
||||
}, 10);
|
||||
}).then(async value => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
async function Component() {
|
||||
const value = await thirdParty('hi');
|
||||
return value;
|
||||
}
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(
|
||||
<Component />,
|
||||
{},
|
||||
{
|
||||
filterStackFrame(filename, functionName) {
|
||||
if (functionName === 'thirdParty') {
|
||||
return false;
|
||||
}
|
||||
return filterStackFrame(filename, functionName);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const readable = new Stream.PassThrough(streamOptions);
|
||||
|
||||
const result = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: {},
|
||||
moduleLoading: {},
|
||||
});
|
||||
stream.pipe(readable);
|
||||
|
||||
expect(await result).toBe('third-party hi');
|
||||
|
||||
await finishLoadingStream(readable);
|
||||
if (
|
||||
__DEV__ &&
|
||||
gate(
|
||||
flags =>
|
||||
flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
|
||||
)
|
||||
) {
|
||||
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2540,
|
||||
40,
|
||||
2519,
|
||||
42,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"awaited": {
|
||||
"end": 0,
|
||||
"env": "Server",
|
||||
"name": "",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2540,
|
||||
40,
|
||||
2519,
|
||||
42,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2526,
|
||||
15,
|
||||
2525,
|
||||
15,
|
||||
],
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2535,
|
||||
19,
|
||||
2534,
|
||||
5,
|
||||
],
|
||||
],
|
||||
"start": 0,
|
||||
"value": {
|
||||
"value": undefined,
|
||||
},
|
||||
},
|
||||
"env": "Server",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2540,
|
||||
40,
|
||||
2519,
|
||||
42,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2526,
|
||||
15,
|
||||
2525,
|
||||
15,
|
||||
],
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2535,
|
||||
19,
|
||||
2534,
|
||||
5,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"awaited": {
|
||||
"end": 0,
|
||||
"env": "Server",
|
||||
"name": "thirdParty",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2540,
|
||||
40,
|
||||
2519,
|
||||
42,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2535,
|
||||
25,
|
||||
2534,
|
||||
5,
|
||||
],
|
||||
],
|
||||
"start": 0,
|
||||
"value": {
|
||||
"value": "third-party hi",
|
||||
},
|
||||
},
|
||||
"env": "Server",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2540,
|
||||
40,
|
||||
2519,
|
||||
42,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
2535,
|
||||
25,
|
||||
2534,
|
||||
5,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,7 @@ 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