Compare commits
26 Commits
pr33584
...
mcp-sync-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f844195 | ||
|
|
c94e8b4461 | ||
|
|
6b04874535 | ||
|
|
1ba1485a65 | ||
|
|
2c1e4e4513 | ||
|
|
e67b4fe22e | ||
|
|
4a523489b7 | ||
|
|
94cf60bede | ||
|
|
bbc13fa17b | ||
|
|
12eaef7ef5 | ||
|
|
c80c69fa96 | ||
|
|
aab72cb1cb | ||
|
|
fa3feba672 | ||
|
|
2a911f27dd | ||
|
|
18ee505e77 | ||
|
|
1d1b26c701 | ||
|
|
fe3f0ec037 | ||
|
|
d70ee32b88 | ||
|
|
6c7b1a1d98 | ||
|
|
ed077194b5 | ||
|
|
643257ca52 | ||
|
|
06e89951be | ||
|
|
79d9aed7ed | ||
|
|
c8822e926b | ||
|
|
a947eba4f2 | ||
|
|
374dfe8edf |
10
.github/workflows/runtime_build_and_test.yml
vendored
10
.github/workflows/runtime_build_and_test.yml
vendored
@@ -301,10 +301,12 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Install runtime dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Install compiler dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: compiler
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
8
.github/workflows/runtime_prereleases.yml
vendored
8
.github/workflows/runtime_prereleases.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -94,19 +94,19 @@ jobs:
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: "GitHub Actions"
|
||||
embed-title: 'Publish of $${{ inputs.release_channel }} release failed'
|
||||
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
|
||||
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
--tags=${{ inputs.tags }} \
|
||||
--publishVersion=${{ inputs.version_to_publish }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
${{ inputs.dry && '--dry' || '' }}
|
||||
- name: Archive released package for debugging
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -8,20 +8,14 @@
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {queryAlgolia} from './utils/algolia';
|
||||
import assertExhaustive from './utils/assertExhaustive';
|
||||
import {convert} from 'html-to-text';
|
||||
import {measurePerformance} from './tools/runtimePerf';
|
||||
import {parseReactComponentTree} from './tools/componentTree';
|
||||
import {
|
||||
runtimePerfTool,
|
||||
componentTreeTool,
|
||||
compileTool,
|
||||
devDocsTool,
|
||||
} from './tools';
|
||||
export type {PassNameType} from './tools';
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
@@ -41,38 +35,27 @@ server.tool(
|
||||
query: z.string(),
|
||||
},
|
||||
async ({query}) => {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
const result = await devDocsTool(query);
|
||||
|
||||
switch (result.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
content: [{type: 'text' as const, text: `No results`}],
|
||||
isError: false,
|
||||
content: result.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: convert(article),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
// Fallback to converting the whole page to text.
|
||||
text: convert($.html()),
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: result.text}],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(result, `Unhandled result ${JSON.stringify(result)}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -93,199 +76,47 @@ server.tool(
|
||||
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
|
||||
},
|
||||
async ({text, passName}) => {
|
||||
const pipelinePasses = new Map<
|
||||
string,
|
||||
Array<PrintedCompilerPipelineValue>
|
||||
>();
|
||||
const recordPass: (
|
||||
result: PrintedCompilerPipelineValue,
|
||||
) => void = result => {
|
||||
const entry = pipelinePasses.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
pipelinePasses.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
recordPass({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
recordPass({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
recordPass({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
const results = await compileTool(text, passName);
|
||||
|
||||
switch (results.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<{type: 'text'; text: string}> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: `${pass.name}\n\n${pass.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
passName,
|
||||
`Unhandled passName option: ${passName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
content: errors.map(err => {
|
||||
isError: false,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text:
|
||||
err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{type: 'text' as const, text: result.code},
|
||||
...requestedPasses,
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'bailout': {
|
||||
return {
|
||||
isError: true,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'error':
|
||||
case 'compile-error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: results.text,
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(
|
||||
results,
|
||||
`Unhandled result ${JSON.stringify(results)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -328,7 +159,7 @@ server.tool(
|
||||
},
|
||||
async ({text, iterations}) => {
|
||||
try {
|
||||
const results = await measurePerformance(text, iterations);
|
||||
const results = await runtimePerfTool(text, iterations);
|
||||
const formattedResults = `
|
||||
# React Component Performance Results
|
||||
|
||||
@@ -387,7 +218,7 @@ server.tool(
|
||||
},
|
||||
async ({url}) => {
|
||||
try {
|
||||
const componentTree = await parseReactComponentTree(url);
|
||||
const componentTree = await componentTreeTool(url);
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -491,7 +322,17 @@ async function main() {
|
||||
console.error('React Compiler MCP Server running on stdio');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
if (require.main !== module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
compileTool,
|
||||
componentTreeTool,
|
||||
devDocsTool,
|
||||
runtimePerfTool,
|
||||
assertExhaustive,
|
||||
};
|
||||
|
||||
203
compiler/packages/react-mcp-server/src/tools/compileTool.ts
Normal file
203
compiler/packages/react-mcp-server/src/tools/compileTool.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import {compile, type PrintedCompilerPipelineValue} from '../compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import assertExhaustive from '../utils/assertExhaustive';
|
||||
|
||||
export type PassNameType =
|
||||
| 'HIR'
|
||||
| 'ReactiveFunction'
|
||||
| 'All'
|
||||
| '@DEBUG'
|
||||
| undefined;
|
||||
|
||||
type CompilerToolOutput =
|
||||
| {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'bailout';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'compile-error';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export async function compileTool(
|
||||
text: string,
|
||||
passName: PassNameType,
|
||||
): Promise<CompilerToolOutput> {
|
||||
const pipelinePasses = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const recordPass: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const entry = pipelinePasses.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
pipelinePasses.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
recordPass({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
recordPass({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
recordPass({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
return {
|
||||
kind: 'compile-error',
|
||||
text: 'Error: Could not compile',
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<string> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push(`${pass.name}\n\n${pass.value}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(passName, `Unhandled passName option: ${passName}`);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
kind: 'bailout',
|
||||
content: errors.map(err => {
|
||||
return err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'success',
|
||||
content: [result.code, ...requestedPasses],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
export async function parseReactComponentTree(url: string): Promise<string> {
|
||||
export async function componentTreeTool(url: string): Promise<string> {
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: 'http://127.0.0.1:9222',
|
||||
49
compiler/packages/react-mcp-server/src/tools/devDocsTool.ts
Normal file
49
compiler/packages/react-mcp-server/src/tools/devDocsTool.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import {convert} from 'html-to-text';
|
||||
import {queryAlgolia} from '../utils/algolia';
|
||||
|
||||
type DevDocsToolOutput =
|
||||
| {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool for querying React dev docs from react.dev
|
||||
* @param query The search query to look up in the React documentation
|
||||
* @returns A promise that resolves to the search results
|
||||
*/
|
||||
export async function devDocsTool(query: string): Promise<DevDocsToolOutput> {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `No results`,
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return convert(article);
|
||||
} else {
|
||||
return convert($.html());
|
||||
}
|
||||
});
|
||||
return {
|
||||
kind: 'success',
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
compiler/packages/react-mcp-server/src/tools/index.ts
Normal file
11
compiler/packages/react-mcp-server/src/tools/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export * from './runtimePerfTool';
|
||||
export * from './componentTreeTool';
|
||||
export * from './compileTool';
|
||||
export * from './devDocsTool';
|
||||
@@ -53,7 +53,7 @@ function delay(time: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function measurePerformance(
|
||||
export async function runtimePerfTool(
|
||||
code: string,
|
||||
iterations: number,
|
||||
): Promise<PerformanceResults> {
|
||||
@@ -69,23 +69,27 @@ export async function measurePerformance(
|
||||
throw new Error('Failed to parse code');
|
||||
}
|
||||
|
||||
const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
() => ({
|
||||
visitor: {
|
||||
ImportDeclaration(
|
||||
path: babel.NodePath<babel.types.ImportDeclaration>,
|
||||
) {
|
||||
const value = path.node.source.value;
|
||||
if (value === 'react' || value === 'react-dom') {
|
||||
path.remove();
|
||||
}
|
||||
const transformResult = await babel.transformFromAstAsync(
|
||||
parsed as babel.types.Node,
|
||||
undefined,
|
||||
{
|
||||
...babelOptions,
|
||||
plugins: [
|
||||
() => ({
|
||||
visitor: {
|
||||
ImportDeclaration(
|
||||
path: babel.NodePath<babel.types.ImportDeclaration>,
|
||||
) {
|
||||
const value = path.node.source.value;
|
||||
if (value === 'react' || value === 'react-dom') {
|
||||
path.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const transpiled = transformResult?.code || undefined;
|
||||
if (!transpiled) {
|
||||
@@ -125,14 +129,16 @@ export async function measurePerformance(
|
||||
|
||||
// ui chaos monkey
|
||||
const selectors = await page.evaluate(() => {
|
||||
window.__INTERACTABLE_SELECTORS__ = [];
|
||||
(window as any).__INTERACTABLE_SELECTORS__ = [];
|
||||
const elements = Array.from(document.querySelectorAll('a')).concat(
|
||||
Array.from(document.querySelectorAll('button')),
|
||||
Array.from(document.querySelectorAll('button')) as any,
|
||||
);
|
||||
for (const el of elements) {
|
||||
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
|
||||
(window as any).__INTERACTABLE_SELECTORS__.push(
|
||||
el.tagName.toLowerCase(),
|
||||
);
|
||||
}
|
||||
return window.__INTERACTABLE_SELECTORS__;
|
||||
return (window as any).__INTERACTABLE_SELECTORS__;
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
38
compiler/packages/react-tools-cli/package.json
Normal file
38
compiler/packages/react-tools-cli/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "react-tools-cli",
|
||||
"version": "0.0.0",
|
||||
"description": "CLI to execute react-mcp-server tools in isolation from the full mcp server",
|
||||
"bin": {
|
||||
"react-tools-cli": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsup"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/facebook/react.git",
|
||||
"directory": "compiler/packages/react-tools-cli"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/parser": "^7.26",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"algoliasearch": "^5.23.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.3.3",
|
||||
"puppeteer": "^24.7.2",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.3.2"
|
||||
}
|
||||
}
|
||||
240
compiler/packages/react-tools-cli/src/index.ts
Normal file
240
compiler/packages/react-tools-cli/src/index.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
PassNameType,
|
||||
assertExhaustive,
|
||||
compileTool,
|
||||
componentTreeTool,
|
||||
devDocsTool,
|
||||
runtimePerfTool,
|
||||
} from 'react-mcp-server/src';
|
||||
|
||||
import yargs from 'yargs';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName('react-tools-cli')
|
||||
.usage('$0 <cmd> [args]')
|
||||
.command(
|
||||
'compile [code] [pass-name]',
|
||||
'Compile React code with React Compiler',
|
||||
yargs => {
|
||||
yargs
|
||||
.positional('code', {
|
||||
type: 'string',
|
||||
describe: 'The code to compile',
|
||||
})
|
||||
.option('pass-name', {
|
||||
type: 'string',
|
||||
choices: ['HIR', 'ReactiveFunction', 'All', '@DEBUG'],
|
||||
describe: 'Compiler pass to run',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const code: string = String(argv['code'] ?? '');
|
||||
const passName: PassNameType = argv['pass-name'] as PassNameType;
|
||||
|
||||
const results = await compileTool(code, passName);
|
||||
|
||||
switch (results.kind) {
|
||||
case 'success': {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: false,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'bailout': {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
case 'error':
|
||||
case 'compile-error':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: results.text,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
default:
|
||||
assertExhaustive(
|
||||
results,
|
||||
`Unhandled result ${JSON.stringify(results)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'query-docs [query]',
|
||||
'Compile React code with React Compiler',
|
||||
yargs => {
|
||||
yargs.positional('query', {
|
||||
type: 'string',
|
||||
describe: 'Browse oficcial React documentation for a given query',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const query: string = String(argv['query'] ?? '');
|
||||
|
||||
const result = await devDocsTool(query);
|
||||
|
||||
switch (result.kind) {
|
||||
case 'success':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: false,
|
||||
content: result.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: text,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: result.text}],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertExhaustive(
|
||||
result,
|
||||
`Unhandled result ${JSON.stringify(result)}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'get-component-tree [url]',
|
||||
'Get the React component tree for a given URL',
|
||||
yargs => {
|
||||
yargs.positional('url', {
|
||||
type: 'string',
|
||||
default: 'https://localhost:3000',
|
||||
describe: 'URL for a React App to get the component tree for',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const url: string = String(argv['url']);
|
||||
try {
|
||||
const result = await componentTreeTool(url);
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
}),
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
'review-code-runtime [code] [iterations',
|
||||
'Get the React component tree for a given URL',
|
||||
yargs => {
|
||||
yargs.positional('code', {
|
||||
type: 'string',
|
||||
default: '',
|
||||
describe: 'React code to run',
|
||||
});
|
||||
yargs.positional('iterations', {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
describe: 'Number of iterations to run the code for',
|
||||
});
|
||||
},
|
||||
async function (argv) {
|
||||
const code: string = String(argv['code']);
|
||||
const iterations: number = Number(argv['iterations']);
|
||||
|
||||
try {
|
||||
const results = await runtimePerfTool(code, iterations);
|
||||
|
||||
const formattedResults = `
|
||||
# React Component Performance Results
|
||||
|
||||
## Mean Render Time
|
||||
${calculateMean(results.renderTime)}
|
||||
|
||||
## Mean Web Vitals
|
||||
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
|
||||
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
|
||||
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
|
||||
|
||||
## Mean React Profiler
|
||||
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
|
||||
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
|
||||
`;
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: formattedResults,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse();
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms'
|
||||
: 'could not collect';
|
||||
}
|
||||
22
compiler/packages/react-tools-cli/tsconfig.json
Normal file
22
compiler/packages/react-tools-cli/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "../",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsxdev",
|
||||
"lib": ["ES2022"],
|
||||
|
||||
// weaken strictness from preset
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noUnusedParameters": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"target": "ES2022",
|
||||
// ideally turn off only during dev, or on a per-file basis
|
||||
"noUnusedLocals": false,
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src/**/*.ts"],
|
||||
}
|
||||
37
compiler/packages/react-tools-cli/tsup.config.ts
Normal file
37
compiler/packages/react-tools-cli/tsup.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: [],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'es2022',
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/`,
|
||||
},
|
||||
});
|
||||
@@ -542,11 +542,6 @@
|
||||
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
|
||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||
|
||||
"@babel/helper-string-parser@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz"
|
||||
@@ -1605,7 +1600,7 @@
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
|
||||
"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
|
||||
version "7.26.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
|
||||
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
|
||||
@@ -1613,14 +1608,6 @@
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
|
||||
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
|
||||
@@ -3745,7 +3732,7 @@ ansi-styles@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
ansi-styles@^6.1.0:
|
||||
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
@@ -4435,6 +4422,15 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-9.0.1.tgz#6f7890f386f6f1f79953adc1f78dec46fcc2d291"
|
||||
integrity sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==
|
||||
dependencies:
|
||||
string-width "^7.2.0"
|
||||
strip-ansi "^7.1.0"
|
||||
wrap-ansi "^9.0.0"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
|
||||
@@ -4963,7 +4959,7 @@ emittery@^0.13.1:
|
||||
resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz"
|
||||
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
||||
|
||||
emoji-regex@^10.2.1:
|
||||
emoji-regex@^10.2.1, emoji-regex@^10.3.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz"
|
||||
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
|
||||
@@ -5798,6 +5794,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-east-asian-width@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389"
|
||||
integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
@@ -9825,6 +9826,15 @@ string-width@^6.1.0:
|
||||
emoji-regex "^10.2.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string-width@^7.0.0, string-width@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc"
|
||||
integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==
|
||||
dependencies:
|
||||
emoji-regex "^10.3.0"
|
||||
get-east-asian-width "^1.0.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
@@ -10551,6 +10561,15 @@ wrap-ansi@^8.1.0:
|
||||
string-width "^5.0.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
wrap-ansi@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e"
|
||||
integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==
|
||||
dependencies:
|
||||
ansi-styles "^6.2.1"
|
||||
string-width "^7.0.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||
@@ -10617,6 +10636,11 @@ yargs-parser@^21.0.1, yargs-parser@^21.1.1:
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs-parser@^22.0.0:
|
||||
version "22.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-22.0.0.tgz#87b82094051b0567717346ecd00fd14804b357c8"
|
||||
integrity sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==
|
||||
|
||||
yargs-unparser@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz"
|
||||
@@ -10670,6 +10694,18 @@ yargs@^17.3.1, yargs@^17.7.1, yargs@^17.7.2:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yargs@^18.0.0:
|
||||
version "18.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-18.0.0.tgz#6c84259806273a746b09f579087b68a3c2d25bd1"
|
||||
integrity sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==
|
||||
dependencies:
|
||||
cliui "^9.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
string-width "^7.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^22.0.0"
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
|
||||
|
||||
@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
|
||||
if (req.headers['cache-control']) {
|
||||
proxiedHeaders['Cache-Control'] = req.get('cache-control');
|
||||
}
|
||||
if (req.get('rsc-request-id')) {
|
||||
proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
|
||||
}
|
||||
|
||||
const requestsPrerender = req.path === '/prerender';
|
||||
|
||||
|
||||
@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
|
||||
|
||||
const React = require('react');
|
||||
|
||||
async function renderApp(res, returnValue, formState, noCache) {
|
||||
const activeDebugChannels =
|
||||
process.env.NODE_ENV === 'development' ? new Map() : null;
|
||||
|
||||
function getDebugChannel(req) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return undefined;
|
||||
}
|
||||
const requestId = req.get('rsc-request-id');
|
||||
if (!requestId) {
|
||||
return undefined;
|
||||
}
|
||||
return activeDebugChannels.get(requestId);
|
||||
}
|
||||
|
||||
async function renderApp(
|
||||
res,
|
||||
returnValue,
|
||||
formState,
|
||||
noCache,
|
||||
promiseForDebugChannel
|
||||
) {
|
||||
const {renderToPipeableStream} = await import(
|
||||
'react-server-dom-webpack/server'
|
||||
);
|
||||
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
|
||||
);
|
||||
// For client-invoked server actions we refresh the tree and return a return value.
|
||||
const payload = {root, returnValue, formState};
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap);
|
||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||
debugChannel: await promiseForDebugChannel,
|
||||
});
|
||||
pipe(res);
|
||||
}
|
||||
|
||||
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
|
||||
if ('prerender' in req.query) {
|
||||
await prerenderApp(res, null, null, noCache);
|
||||
} else {
|
||||
await renderApp(res, null, null, noCache);
|
||||
await renderApp(res, null, null, noCache, getDebugChannel(req));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// We handle the error on the client
|
||||
}
|
||||
// Refresh the client and return the value
|
||||
renderApp(res, result, null, noCache);
|
||||
renderApp(res, result, null, noCache, getDebugChannel(req));
|
||||
} else {
|
||||
// This is the progressive enhancement case
|
||||
const UndiciRequest = require('undici').Request;
|
||||
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
|
||||
// Wait for any mutations
|
||||
const result = await action();
|
||||
const formState = decodeFormState(result, formData);
|
||||
renderApp(res, null, formState, noCache);
|
||||
renderApp(res, null, formState, noCache, undefined);
|
||||
} catch (x) {
|
||||
const {setServerState} = await import('../src/ServerState.js');
|
||||
setServerState('Error: ' + x.message);
|
||||
renderApp(res, null, null, noCache);
|
||||
renderApp(res, null, null, noCache, undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(3001, () => {
|
||||
const httpServer = app.listen(3001, () => {
|
||||
console.log('Regional Flight Server listening on port 3001...');
|
||||
});
|
||||
|
||||
@@ -346,3 +368,27 @@ app.on('error', function (error) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Open a websocket server for Debug information
|
||||
const WebSocket = require('ws');
|
||||
const webSocketServer = new WebSocket.Server({noServer: true});
|
||||
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const DEBUG_CHANNEL_PATH = '/debug-channel?';
|
||||
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
|
||||
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
|
||||
const promiseForWs = new Promise(resolve => {
|
||||
webSocketServer.handleUpgrade(request, socket, head, ws => {
|
||||
ws.on('close', () => {
|
||||
activeDebugChannels.delete(requestId);
|
||||
});
|
||||
resolve(ws);
|
||||
});
|
||||
});
|
||||
activeDebugChannels.set(requestId, promiseForWs);
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,12 +33,22 @@ function Foo({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
async function delayedError(text, ms) {
|
||||
return new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(text)), ms)
|
||||
);
|
||||
}
|
||||
|
||||
async function delay(text, ms) {
|
||||
return new Promise(resolve => setTimeout(() => resolve(text), ms));
|
||||
}
|
||||
|
||||
async function delayTwice() {
|
||||
await delay('', 20);
|
||||
try {
|
||||
await delayedError('Delayed exception', 20);
|
||||
} catch (x) {
|
||||
// Ignored
|
||||
}
|
||||
await delay('', 10);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,17 +42,43 @@ function Shell({data}) {
|
||||
}
|
||||
|
||||
async function hydrateApp() {
|
||||
const {root, returnValue, formState} = await createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
let response;
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
typeof WebSocketStream === 'function'
|
||||
) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const wss = new WebSocketStream(
|
||||
'ws://localhost:3001/debug-channel?' + requestId
|
||||
);
|
||||
const debugChannel = await wss.opened;
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
'rsc-request-id': requestId,
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
debugChannel,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response = createFromFetch(
|
||||
fetch('/', {
|
||||
headers: {
|
||||
Accept: 'text/x-component',
|
||||
},
|
||||
}),
|
||||
{
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
}
|
||||
);
|
||||
}
|
||||
const {root, returnValue, formState} = await response;
|
||||
|
||||
ReactDOM.hydrateRoot(
|
||||
document,
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
unstable_addTransitionType as addTransitionType,
|
||||
use,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
|
||||
362
packages/react-client/src/ReactFlightClient.js
vendored
362
packages/react-client/src/ReactFlightClient.js
vendored
@@ -79,7 +79,9 @@ import {
|
||||
logDedupedComponentRender,
|
||||
logComponentErrored,
|
||||
logIOInfo,
|
||||
logIOInfoErrored,
|
||||
logComponentAwait,
|
||||
logComponentAwaitErrored,
|
||||
} from './ReactFlightPerformanceTrack';
|
||||
|
||||
import {
|
||||
@@ -96,6 +98,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
|
||||
|
||||
import {injectInternals} from './ReactFlightClientDevToolsHook';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
|
||||
import isArray from 'shared/isArray';
|
||||
@@ -155,12 +159,12 @@ const RESOLVED_MODEL = 'resolved_model';
|
||||
const RESOLVED_MODULE = 'resolved_module';
|
||||
const INITIALIZED = 'fulfilled';
|
||||
const ERRORED = 'rejected';
|
||||
const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes.
|
||||
|
||||
type PendingChunk<T> = {
|
||||
status: 'pending',
|
||||
value: null | Array<(T) => mixed>,
|
||||
reason: null | Array<(mixed) => mixed>,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -169,7 +173,6 @@ type BlockedChunk<T> = {
|
||||
status: 'blocked',
|
||||
value: null | Array<(T) => mixed>,
|
||||
reason: null | Array<(mixed) => mixed>,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -177,8 +180,7 @@ type BlockedChunk<T> = {
|
||||
type ResolvedModelChunk<T> = {
|
||||
status: 'resolved_model',
|
||||
value: UninitializedModel,
|
||||
reason: null,
|
||||
_response: Response,
|
||||
reason: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -187,7 +189,6 @@ type ResolvedModuleChunk<T> = {
|
||||
status: 'resolved_module',
|
||||
value: ClientReference<T>,
|
||||
reason: null,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -196,7 +197,6 @@ type InitializedChunk<T> = {
|
||||
status: 'fulfilled',
|
||||
value: T,
|
||||
reason: null | FlightStreamController,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -207,7 +207,6 @@ type InitializedStreamChunk<
|
||||
status: 'fulfilled',
|
||||
value: T,
|
||||
reason: FlightStreamController,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -216,7 +215,14 @@ type ErroredChunk<T> = {
|
||||
status: 'rejected',
|
||||
value: null,
|
||||
reason: mixed,
|
||||
_response: Response,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
};
|
||||
type HaltedChunk<T> = {
|
||||
status: 'halted',
|
||||
value: null,
|
||||
reason: null,
|
||||
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
|
||||
_debugInfo?: null | ReactDebugInfo, // DEV-only
|
||||
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
|
||||
@@ -227,19 +233,14 @@ type SomeChunk<T> =
|
||||
| ResolvedModelChunk<T>
|
||||
| ResolvedModuleChunk<T>
|
||||
| InitializedChunk<T>
|
||||
| ErroredChunk<T>;
|
||||
| ErroredChunk<T>
|
||||
| HaltedChunk<T>;
|
||||
|
||||
// $FlowFixMe[missing-this-annot]
|
||||
function ReactPromise(
|
||||
status: any,
|
||||
value: any,
|
||||
reason: any,
|
||||
response: Response,
|
||||
) {
|
||||
function ReactPromise(status: any, value: any, reason: any) {
|
||||
this.status = status;
|
||||
this.value = value;
|
||||
this.reason = reason;
|
||||
this._response = response;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
this._children = [];
|
||||
}
|
||||
@@ -266,7 +267,11 @@ ReactPromise.prototype.then = function <T>(
|
||||
initializeModuleChunk(chunk);
|
||||
break;
|
||||
}
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
enableAsyncDebugInfo &&
|
||||
(typeof resolve !== 'function' || !(resolve: any).isReactInternalListener)
|
||||
) {
|
||||
// Because only native Promises get picked up when we're awaiting we need to wrap
|
||||
// this in a native Promise in DEV. This means that these callbacks are no longer sync
|
||||
// but the lazy initialization is still sync and the .value can be inspected after,
|
||||
@@ -307,6 +312,9 @@ ReactPromise.prototype.then = function <T>(
|
||||
chunk.reason.push(reject);
|
||||
}
|
||||
break;
|
||||
case HALTED: {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (reject) {
|
||||
reject(chunk.reason);
|
||||
@@ -320,6 +328,8 @@ export type FindSourceMapURLCallback = (
|
||||
environmentName: string,
|
||||
) => null | string;
|
||||
|
||||
export type DebugChannelCallback = (message: string) => void;
|
||||
|
||||
export type Response = {
|
||||
_bundlerConfig: ServerConsumerModuleMap,
|
||||
_serverReferenceConfig: null | ServerManifest,
|
||||
@@ -343,6 +353,7 @@ export type Response = {
|
||||
_debugRootStack?: null | Error, // DEV-only
|
||||
_debugRootTask?: null | ConsoleTask, // DEV-only
|
||||
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
|
||||
_debugChannel?: void | DebugChannelCallback, // DEV-only
|
||||
_replayConsole: boolean, // DEV-only
|
||||
_rootEnvironmentName: string, // DEV-only, the requested environment name.
|
||||
};
|
||||
@@ -364,6 +375,7 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
|
||||
return chunk.value;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
case HALTED:
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw ((chunk: any): Thenable<T>);
|
||||
default:
|
||||
@@ -378,12 +390,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
|
||||
|
||||
function createPendingChunk<T>(response: Response): PendingChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(PENDING, null, null, response);
|
||||
return new ReactPromise(PENDING, null, null);
|
||||
}
|
||||
|
||||
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(BLOCKED, null, null, response);
|
||||
return new ReactPromise(BLOCKED, null, null);
|
||||
}
|
||||
|
||||
function createErrorChunk<T>(
|
||||
@@ -391,7 +403,7 @@ function createErrorChunk<T>(
|
||||
error: mixed,
|
||||
): ErroredChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(ERRORED, null, error, response);
|
||||
return new ReactPromise(ERRORED, null, error);
|
||||
}
|
||||
|
||||
function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
|
||||
@@ -463,7 +475,7 @@ function createResolvedModelChunk<T>(
|
||||
value: UninitializedModel,
|
||||
): ResolvedModelChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODEL, value, null, response);
|
||||
return new ReactPromise(RESOLVED_MODEL, value, response);
|
||||
}
|
||||
|
||||
function createResolvedModuleChunk<T>(
|
||||
@@ -471,7 +483,7 @@ function createResolvedModuleChunk<T>(
|
||||
value: ClientReference<T>,
|
||||
): ResolvedModuleChunk<T> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODULE, value, null, response);
|
||||
return new ReactPromise(RESOLVED_MODULE, value, null);
|
||||
}
|
||||
|
||||
function createInitializedTextChunk(
|
||||
@@ -479,7 +491,7 @@ function createInitializedTextChunk(
|
||||
value: string,
|
||||
): InitializedChunk<string> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, null, response);
|
||||
return new ReactPromise(INITIALIZED, value, null);
|
||||
}
|
||||
|
||||
function createInitializedBufferChunk(
|
||||
@@ -487,7 +499,7 @@ function createInitializedBufferChunk(
|
||||
value: $ArrayBufferView | ArrayBuffer,
|
||||
): InitializedChunk<Uint8Array> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, null, response);
|
||||
return new ReactPromise(INITIALIZED, value, null);
|
||||
}
|
||||
|
||||
function createInitializedIteratorResultChunk<T>(
|
||||
@@ -496,12 +508,7 @@ function createInitializedIteratorResultChunk<T>(
|
||||
done: boolean,
|
||||
): InitializedChunk<IteratorResult<T, T>> {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: done, value: value},
|
||||
null,
|
||||
response,
|
||||
);
|
||||
return new ReactPromise(INITIALIZED, {done: done, value: value}, null);
|
||||
}
|
||||
|
||||
function createInitializedStreamChunk<
|
||||
@@ -514,7 +521,7 @@ function createInitializedStreamChunk<
|
||||
// We use the reason field to stash the controller since we already have that
|
||||
// field. It's a bit of a hack but efficient.
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(INITIALIZED, value, controller, response);
|
||||
return new ReactPromise(INITIALIZED, value, controller);
|
||||
}
|
||||
|
||||
function createResolvedIteratorResultChunk<T>(
|
||||
@@ -526,10 +533,11 @@ function createResolvedIteratorResultChunk<T>(
|
||||
const iteratorResultJSON =
|
||||
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response);
|
||||
return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response);
|
||||
}
|
||||
|
||||
function resolveIteratorResultChunk<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<IteratorResult<T, T>>,
|
||||
value: UninitializedModel,
|
||||
done: boolean,
|
||||
@@ -537,10 +545,11 @@ function resolveIteratorResultChunk<T>(
|
||||
// To reuse code as much code as possible we add the wrapper element as part of the JSON.
|
||||
const iteratorResultJSON =
|
||||
(done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
|
||||
resolveModelChunk(chunk, iteratorResultJSON);
|
||||
resolveModelChunk(response, chunk, iteratorResultJSON);
|
||||
}
|
||||
|
||||
function resolveModelChunk<T>(
|
||||
response: Response,
|
||||
chunk: SomeChunk<T>,
|
||||
value: UninitializedModel,
|
||||
): void {
|
||||
@@ -557,6 +566,7 @@ function resolveModelChunk<T>(
|
||||
const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
|
||||
resolvedChunk.status = RESOLVED_MODEL;
|
||||
resolvedChunk.value = value;
|
||||
resolvedChunk.reason = response;
|
||||
if (resolveListeners !== null) {
|
||||
// This is unfortunate that we're reading this eagerly if
|
||||
// we already have listeners attached since they might no
|
||||
@@ -602,6 +612,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
initializingHandler = null;
|
||||
|
||||
const resolvedModel = chunk.value;
|
||||
const response = chunk.reason;
|
||||
|
||||
// We go to the BLOCKED state until we've fully resolved this.
|
||||
// We do this before parsing in case we try to initialize the same chunk
|
||||
@@ -616,7 +627,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
}
|
||||
|
||||
try {
|
||||
const value: T = parseModel(chunk._response, resolvedModel);
|
||||
const value: T = parseModel(response, resolvedModel);
|
||||
// Invoke any listeners added while resolving this model. I.e. cyclic
|
||||
// references. This may or may not fully resolve the model depending on
|
||||
// if they were blocked.
|
||||
@@ -679,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
|
||||
triggerErrorOnChunk(chunk, error);
|
||||
}
|
||||
});
|
||||
if (__DEV__) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel !== undefined) {
|
||||
// If we don't have any more ways of reading data, we don't have to send any
|
||||
// more neither. So we close the writable side.
|
||||
debugChannel('');
|
||||
response._debugChannel = undefined;
|
||||
}
|
||||
}
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
markAllTracksInOrder();
|
||||
flushComponentPerformance(
|
||||
@@ -1052,6 +1072,10 @@ function waitForReference<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use to avoid the microtask resolution in DEV.
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
(fulfill: any).isReactInternalListener = true;
|
||||
}
|
||||
|
||||
function reject(error: mixed): void {
|
||||
if (handler.errored) {
|
||||
@@ -1359,6 +1383,7 @@ function getOutlinedModel<T>(
|
||||
return chunkValue;
|
||||
case PENDING:
|
||||
case BLOCKED:
|
||||
case HALTED:
|
||||
return waitForReference(chunk, parentObject, key, response, map, path);
|
||||
default:
|
||||
// This is an error. Instead of erroring directly, we're going to encode this on
|
||||
@@ -1406,6 +1431,17 @@ function createFormData(
|
||||
return formData;
|
||||
}
|
||||
|
||||
function applyConstructor(
|
||||
response: Response,
|
||||
model: Function,
|
||||
parentObject: Object,
|
||||
key: string,
|
||||
): void {
|
||||
Object.setPrototypeOf(parentObject, model.prototype);
|
||||
// Delete the property. It was just a placeholder.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractIterator(response: Response, model: Array<any>): Iterator<any> {
|
||||
// $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
|
||||
return model[Symbol.iterator]();
|
||||
@@ -1462,10 +1498,6 @@ function parseModelString(
|
||||
}
|
||||
case '@': {
|
||||
// Promise
|
||||
if (value.length === 2) {
|
||||
// Infinite promise that never resolves.
|
||||
return new Promise(() => {});
|
||||
}
|
||||
const id = parseInt(value.slice(2), 16);
|
||||
const chunk = getChunk(response, id);
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
@@ -1586,16 +1618,60 @@ function parseModelString(
|
||||
// BigInt
|
||||
return BigInt(value.slice(2));
|
||||
}
|
||||
case 'P': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow debug objects to specify themselves as instances of
|
||||
// another constructor.
|
||||
const ref = value.slice(2);
|
||||
return getOutlinedModel(
|
||||
response,
|
||||
ref,
|
||||
parentObject,
|
||||
key,
|
||||
applyConstructor,
|
||||
);
|
||||
}
|
||||
//Fallthrough
|
||||
}
|
||||
case 'E': {
|
||||
if (__DEV__) {
|
||||
// In DEV mode we allow indirect eval to produce functions for logging.
|
||||
// This should not compile to eval() because then it has local scope access.
|
||||
const code = value.slice(2);
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(value.slice(2));
|
||||
return (0, eval)(code);
|
||||
} catch (x) {
|
||||
// We currently use this to express functions so we fail parsing it,
|
||||
// let's just return a blank function as a place holder.
|
||||
if (code.startsWith('(async function')) {
|
||||
const idx = code.indexOf('(', 15);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(15, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(
|
||||
'({' + JSON.stringify(name) + ':async function(){}})',
|
||||
)[name];
|
||||
}
|
||||
} else if (code.startsWith('(function')) {
|
||||
const idx = code.indexOf('(', 9);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(9, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)(
|
||||
'({' + JSON.stringify(name) + ':function(){}})',
|
||||
)[name];
|
||||
}
|
||||
} else if (code.startsWith('(class')) {
|
||||
const idx = code.indexOf('{', 6);
|
||||
if (idx !== -1) {
|
||||
const name = code.slice(6, idx).trim();
|
||||
// eslint-disable-next-line no-eval
|
||||
return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[
|
||||
name
|
||||
];
|
||||
}
|
||||
}
|
||||
return function () {};
|
||||
}
|
||||
}
|
||||
@@ -1603,17 +1679,21 @@ function parseModelString(
|
||||
}
|
||||
case 'Y': {
|
||||
if (__DEV__) {
|
||||
if (value.length > 2) {
|
||||
const debugChannel = response._debugChannel;
|
||||
if (debugChannel) {
|
||||
const ref = value.slice(2);
|
||||
debugChannel('R:' + ref); // Release this reference immediately
|
||||
}
|
||||
}
|
||||
|
||||
// In DEV mode we encode omitted objects in logs as a getter that throws
|
||||
// so that when you try to access it on the client, you know why that
|
||||
// happened.
|
||||
Object.defineProperty(parentObject, key, {
|
||||
get: function () {
|
||||
// TODO: We should ideally throw here to indicate a difference.
|
||||
return (
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.'
|
||||
);
|
||||
return OMITTED_PROP_ERROR;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
@@ -1670,9 +1750,10 @@ function ResponseInstance(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
) {
|
||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||
this._bundlerConfig = bundlerConfig;
|
||||
@@ -1727,6 +1808,7 @@ function ResponseInstance(
|
||||
);
|
||||
}
|
||||
this._debugFindSourceMapURL = findSourceMapURL;
|
||||
this._debugChannel = debugChannel;
|
||||
this._replayConsole = replayConsole;
|
||||
this._rootEnvironmentName = rootEnv;
|
||||
}
|
||||
@@ -1742,9 +1824,10 @@ export function createResponse(
|
||||
encodeFormAction: void | EncodeFormActionCallback,
|
||||
nonce: void | string,
|
||||
temporaryReferences: void | TemporaryReferenceSet,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback,
|
||||
replayConsole: boolean,
|
||||
environmentName: void | string,
|
||||
findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
|
||||
replayConsole: boolean, // DEV-only
|
||||
environmentName: void | string, // DEV-only
|
||||
debugChannel: void | DebugChannelCallback, // DEV-only
|
||||
): Response {
|
||||
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
|
||||
return new ResponseInstance(
|
||||
@@ -1758,9 +1841,26 @@ export function createResponse(
|
||||
findSourceMapURL,
|
||||
replayConsole,
|
||||
environmentName,
|
||||
debugChannel,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDebugHalt(response: Response, id: number): void {
|
||||
const chunks = response._chunks;
|
||||
let chunk = chunks.get(id);
|
||||
if (!chunk) {
|
||||
chunks.set(id, (chunk = createPendingChunk(response)));
|
||||
} else {
|
||||
}
|
||||
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
|
||||
return;
|
||||
}
|
||||
const haltedChunk: HaltedChunk<any> = (chunk: any);
|
||||
haltedChunk.status = HALTED;
|
||||
haltedChunk.value = null;
|
||||
haltedChunk.reason = null;
|
||||
}
|
||||
|
||||
function resolveModel(
|
||||
response: Response,
|
||||
id: number,
|
||||
@@ -1771,7 +1871,7 @@ function resolveModel(
|
||||
if (!chunk) {
|
||||
chunks.set(id, createResolvedModelChunk(response, model));
|
||||
} else {
|
||||
resolveModelChunk(chunk, model);
|
||||
resolveModelChunk(response, chunk, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1945,7 +2045,7 @@ function startReadableStream<T>(
|
||||
// to synchronous emitting.
|
||||
previousBlockedChunk = null;
|
||||
}
|
||||
resolveModelChunk(chunk, json);
|
||||
resolveModelChunk(response, chunk, json);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -2033,7 +2133,12 @@ function startAsyncIterable<T>(
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex],
|
||||
value,
|
||||
false,
|
||||
);
|
||||
}
|
||||
nextWriteIndex++;
|
||||
},
|
||||
@@ -2046,12 +2151,18 @@ function startAsyncIterable<T>(
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex],
|
||||
value,
|
||||
true,
|
||||
);
|
||||
}
|
||||
nextWriteIndex++;
|
||||
while (nextWriteIndex < buffer.length) {
|
||||
// In generators, any extra reads from the iterator have the value undefined.
|
||||
resolveIteratorResultChunk(
|
||||
response,
|
||||
buffer[nextWriteIndex++],
|
||||
'"$undefined"',
|
||||
true,
|
||||
@@ -2069,32 +2180,33 @@ function startAsyncIterable<T>(
|
||||
}
|
||||
},
|
||||
};
|
||||
const iterable: $AsyncIterable<T, T, void> = {
|
||||
[ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
|
||||
let nextReadIndex = 0;
|
||||
return createIterator(arg => {
|
||||
if (arg !== undefined) {
|
||||
throw new Error(
|
||||
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
|
||||
|
||||
const iterable: $AsyncIterable<T, T, void> = ({}: any);
|
||||
// $FlowFixMe[cannot-write]
|
||||
iterable[ASYNC_ITERATOR] = (): $AsyncIterator<T, T, void> => {
|
||||
let nextReadIndex = 0;
|
||||
return createIterator(arg => {
|
||||
if (arg !== undefined) {
|
||||
throw new Error(
|
||||
'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
|
||||
);
|
||||
}
|
||||
if (nextReadIndex === buffer.length) {
|
||||
if (closed) {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: true, value: undefined},
|
||||
null,
|
||||
);
|
||||
}
|
||||
if (nextReadIndex === buffer.length) {
|
||||
if (closed) {
|
||||
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||
return new ReactPromise(
|
||||
INITIALIZED,
|
||||
{done: true, value: undefined},
|
||||
null,
|
||||
response,
|
||||
);
|
||||
}
|
||||
buffer[nextReadIndex] =
|
||||
createPendingChunk<IteratorResult<T, T>>(response);
|
||||
}
|
||||
return buffer[nextReadIndex++];
|
||||
});
|
||||
},
|
||||
buffer[nextReadIndex] =
|
||||
createPendingChunk<IteratorResult<T, T>>(response);
|
||||
}
|
||||
return buffer[nextReadIndex++];
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after
|
||||
// reading through the end, but currently we favor code size over this optimization.
|
||||
resolveStream(
|
||||
@@ -2627,9 +2739,15 @@ function initializeFakeStack(
|
||||
// $FlowFixMe[cannot-write]
|
||||
debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
|
||||
}
|
||||
if (debugInfo.owner != null) {
|
||||
const owner = debugInfo.owner;
|
||||
if (owner != null) {
|
||||
// Initialize any owners not yet initialized.
|
||||
initializeFakeStack(response, debugInfo.owner);
|
||||
initializeFakeStack(response, owner);
|
||||
if (owner.debugLocation === undefined && debugInfo.debugStack != null) {
|
||||
// If we are the child of this owner, then the owner should be the bottom frame
|
||||
// our stack. We can use it as the implied location of the owner.
|
||||
owner.debugLocation = debugInfo.debugStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2816,7 +2934,29 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
|
||||
// $FlowFixMe[cannot-write]
|
||||
ioInfo.end += response._timeOrigin;
|
||||
|
||||
logIOInfo(ioInfo, response._rootEnvironmentName);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = ioInfo.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logIOInfo(ioInfo, env, thenable.value);
|
||||
break;
|
||||
case ERRORED:
|
||||
logIOInfoErrored(ioInfo, env, thenable.reason);
|
||||
break;
|
||||
default:
|
||||
// If we haven't resolved the Promise yet, wait to log until have so we can include
|
||||
// its data in the log.
|
||||
promise.then(
|
||||
logIOInfo.bind(null, ioInfo, env),
|
||||
logIOInfoErrored.bind(null, ioInfo, env),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logIOInfo(ioInfo, env, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIOInfo(
|
||||
@@ -2831,7 +2971,7 @@ function resolveIOInfo(
|
||||
chunks.set(id, chunk);
|
||||
initializeModelChunk(chunk);
|
||||
} else {
|
||||
resolveModelChunk(chunk, model);
|
||||
resolveModelChunk(response, chunk, model);
|
||||
if (chunk.status === RESOLVED_MODEL) {
|
||||
initializeModelChunk(chunk);
|
||||
}
|
||||
@@ -3100,13 +3240,55 @@ function flushComponentPerformance(
|
||||
}
|
||||
// $FlowFixMe: Refined.
|
||||
const asyncInfo: ReactAsyncInfo = candidateInfo;
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = asyncInfo.awaited.value;
|
||||
if (promise) {
|
||||
const thenable: Thenable<mixed> = (promise: any);
|
||||
switch (thenable.status) {
|
||||
case INITIALIZED:
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.value,
|
||||
);
|
||||
break;
|
||||
case ERRORED:
|
||||
logComponentAwaitErrored(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
thenable.reason,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// We assume that we should have received the data by now since this is logged at the
|
||||
// end of the response stream. This is more sensitive to ordering so we don't wait
|
||||
// to log it.
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
logComponentAwait(
|
||||
asyncInfo,
|
||||
trackIdx,
|
||||
time,
|
||||
endTime,
|
||||
env,
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3329,6 +3511,10 @@ function processFullStringRow(
|
||||
}
|
||||
// Fallthrough
|
||||
default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ {
|
||||
if (__DEV__ && row === '') {
|
||||
resolveDebugHalt(response, id);
|
||||
return;
|
||||
}
|
||||
// We assume anything else is JSON.
|
||||
resolveModel(response, id, row);
|
||||
return;
|
||||
|
||||
@@ -17,14 +17,143 @@ import type {
|
||||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
typeof console !== 'undefined' &&
|
||||
typeof console.timeStamp === 'function';
|
||||
typeof console.timeStamp === 'function' &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function';
|
||||
|
||||
const IO_TRACK = 'Server Requests ⚛';
|
||||
const COMPONENTS_TRACK = 'Server Components ⚛';
|
||||
|
||||
const EMPTY_ARRAY = 0;
|
||||
const COMPLEX_ARRAY = 1;
|
||||
const PRIMITIVE_ARRAY = 2; // Primitive values only
|
||||
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
|
||||
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
|
||||
let kind = EMPTY_ARRAY;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const value = array[i];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (
|
||||
isArray(value) &&
|
||||
value.length === 2 &&
|
||||
typeof value[0] === 'string'
|
||||
) {
|
||||
// Key value tuple
|
||||
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
kind = ENTRIES_ARRAY;
|
||||
} else {
|
||||
return COMPLEX_ARRAY;
|
||||
}
|
||||
} else if (typeof value === 'function') {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (typeof value === 'string' && value.length > 50) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
|
||||
return COMPLEX_ARRAY;
|
||||
} else {
|
||||
kind = PRIMITIVE_ARRAY;
|
||||
}
|
||||
}
|
||||
return kind;
|
||||
}
|
||||
|
||||
function addObjectToProperties(
|
||||
object: Object,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
for (const key in object) {
|
||||
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
|
||||
const value = object[key];
|
||||
addValueToProperties(key, value, properties, indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addValueToProperties(
|
||||
propertyName: string,
|
||||
value: mixed,
|
||||
properties: Array<[string, string]>,
|
||||
indent: number,
|
||||
): void {
|
||||
let desc;
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
desc = 'null';
|
||||
break;
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const objectToString = Object.prototype.toString.call(value);
|
||||
let objectName = objectToString.slice(8, objectToString.length - 1);
|
||||
if (objectName === 'Array') {
|
||||
const array: Array<any> = (value: any);
|
||||
const kind = getArrayKind(array);
|
||||
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
|
||||
desc = JSON.stringify(array);
|
||||
break;
|
||||
} else if (kind === ENTRIES_ARRAY) {
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const entry = array[i];
|
||||
addValueToProperties(entry[0], entry[1], properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (objectName === 'Object') {
|
||||
const proto: any = Object.getPrototypeOf(value);
|
||||
if (proto && typeof proto.constructor === 'function') {
|
||||
objectName = proto.constructor.name;
|
||||
}
|
||||
}
|
||||
properties.push([
|
||||
'\xa0\xa0'.repeat(indent) + propertyName,
|
||||
objectName === 'Object' ? '' : objectName,
|
||||
]);
|
||||
if (indent < 3) {
|
||||
addObjectToProperties(value, properties, indent + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'function':
|
||||
if (value.name === '') {
|
||||
desc = '() => {}';
|
||||
} else {
|
||||
desc = value.name + '() {}';
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (value === OMITTED_PROP_ERROR) {
|
||||
desc = '...';
|
||||
} else {
|
||||
desc = JSON.stringify(value);
|
||||
}
|
||||
break;
|
||||
case 'undefined':
|
||||
desc = 'undefined';
|
||||
break;
|
||||
case 'boolean':
|
||||
desc = value ? 'true' : 'false';
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
desc = String(value);
|
||||
}
|
||||
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
|
||||
}
|
||||
|
||||
export function markAllTracksInOrder() {
|
||||
if (supportsUserTiming) {
|
||||
// Ensure we create the Server Component track groups earlier than the Client Scheduler
|
||||
@@ -133,12 +262,7 @@ export function logComponentErrored(
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
if (
|
||||
__DEV__ &&
|
||||
typeof performance !== 'undefined' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
if (__DEV__) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
@@ -228,12 +352,68 @@ function getIOColor(
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentAwaitErrored(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
const name = asyncInfo.awaited.name;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
'await ' +
|
||||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentAwait(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming && endTime > 0) {
|
||||
const env = asyncInfo.env;
|
||||
@@ -245,17 +425,26 @@ export function logComponentAwait(
|
||||
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
||||
const debugTask = asyncInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
@@ -270,7 +459,63 @@ export function logComponentAwait(
|
||||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
||||
export function logIOInfoErrored(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
const name = ioInfo.name;
|
||||
const env = ioInfo.env;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const debugTask = ioInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Rejected', message]];
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
tooltipText: entryName + ' Rejected',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logIOInfo(
|
||||
ioInfo: ReactIOInfo,
|
||||
rootEnv: string,
|
||||
value: mixed,
|
||||
): void {
|
||||
const startTime = ioInfo.start;
|
||||
const endTime = ioInfo.end;
|
||||
if (supportsUserTiming && endTime >= 0) {
|
||||
@@ -282,17 +527,25 @@ export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
|
||||
const debugTask = ioInfo.debugTask;
|
||||
const color = getIOColor(name);
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
addObjectToProperties(value, properties, 0);
|
||||
} else if (value !== undefined) {
|
||||
addValueToProperties('Resolved', value, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
endTime,
|
||||
IO_TRACK,
|
||||
undefined,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: IO_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
||||
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
13
packages/react-client/src/ReactFlightPropertyAccess.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export const OMITTED_PROP_ERROR =
|
||||
'This object has been omitted by React in the console log ' +
|
||||
'to avoid sending too much data from the server. Try logging smaller ' +
|
||||
'or more specific objects.';
|
||||
@@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) {
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
@@ -3208,12 +3208,29 @@ describe('ReactFlight', () => {
|
||||
return 'hello';
|
||||
}
|
||||
|
||||
class MyClass {
|
||||
constructor() {
|
||||
this.x = 1;
|
||||
}
|
||||
method() {}
|
||||
get y() {
|
||||
return this.x + 1;
|
||||
}
|
||||
get z() {
|
||||
return this.x + 5;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(MyClass.prototype, 'y', {enumerable: true});
|
||||
|
||||
function ServerComponent() {
|
||||
console.log('hi', {
|
||||
prop: 123,
|
||||
fn: foo,
|
||||
map: new Map([['foo', foo]]),
|
||||
promise: new Promise(() => {}),
|
||||
promise: Promise.resolve('yo'),
|
||||
infinitePromise: new Promise(() => {}),
|
||||
Class: MyClass,
|
||||
instance: new MyClass(),
|
||||
});
|
||||
throw new Error('err');
|
||||
}
|
||||
@@ -3258,9 +3275,14 @@ describe('ReactFlight', () => {
|
||||
});
|
||||
ownerStacks = [];
|
||||
|
||||
// Let the Promises resolve.
|
||||
await 0;
|
||||
await 0;
|
||||
await 0;
|
||||
|
||||
// The error should not actually get logged because we're not awaiting the root
|
||||
// so it's not thrown but the server log also shouldn't be replayed.
|
||||
await ReactNoopFlightClient.read(transport);
|
||||
await ReactNoopFlightClient.read(transport, {close: true});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
@@ -3276,9 +3298,40 @@ describe('ReactFlight', () => {
|
||||
expect(typeof loggedFn2).toBe('function');
|
||||
expect(loggedFn2).not.toBe(foo);
|
||||
expect(loggedFn2.toString()).toBe(foo.toString());
|
||||
expect(loggedFn2).toBe(loggedFn);
|
||||
|
||||
const promise = mockConsoleLog.mock.calls[0][1].promise;
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(await promise).toBe('yo');
|
||||
|
||||
const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise;
|
||||
expect(infinitePromise).toBeInstanceOf(Promise);
|
||||
let resolved = false;
|
||||
infinitePromise.then(
|
||||
() => (resolved = true),
|
||||
x => {
|
||||
console.error(x);
|
||||
resolved = true;
|
||||
},
|
||||
);
|
||||
await 0;
|
||||
await 0;
|
||||
await 0;
|
||||
// This should not reject upon aborting the stream.
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
const Class = mockConsoleLog.mock.calls[0][1].Class;
|
||||
const instance = mockConsoleLog.mock.calls[0][1].instance;
|
||||
expect(typeof Class).toBe('function');
|
||||
expect(Class.prototype.constructor).toBe(Class);
|
||||
expect(instance instanceof Class).toBe(true);
|
||||
expect(Object.getPrototypeOf(instance)).toBe(Class.prototype);
|
||||
expect(instance.x).toBe(1);
|
||||
expect(instance.hasOwnProperty('y')).toBe(true);
|
||||
expect(instance.y).toBe(2); // Enumerable getter was reified
|
||||
expect(instance.hasOwnProperty('z')).toBe(false);
|
||||
expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class
|
||||
expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString()
|
||||
|
||||
expect(ownerStacks).toEqual(['\n in App (at **)']);
|
||||
});
|
||||
@@ -3327,19 +3380,10 @@ describe('ReactFlight', () => {
|
||||
await ReactNoopFlightClient.read(transport);
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
// TODO: Support cyclic objects in console encoding.
|
||||
// expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
// const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
|
||||
// expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
|
||||
// expect(cyclic2.cycle).toBe(cyclic2);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe(
|
||||
'Unknown Value: React could not send it from the server.',
|
||||
);
|
||||
expect(mockConsoleLog.mock.calls[0][1].message).toBe(
|
||||
'Converting circular structure to JSON\n' +
|
||||
" --> starting at object with constructor 'Object'\n" +
|
||||
" --- property 'cycle' closes the circle",
|
||||
);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
|
||||
expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
|
||||
expect(cyclic2.cycle).toBe(cyclic2);
|
||||
});
|
||||
|
||||
// @gate !__DEV__ || enableComponentPerformanceTrack
|
||||
|
||||
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
139
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
if (typeof Blob === 'undefined') {
|
||||
global.Blob = require('buffer').Blob;
|
||||
}
|
||||
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
|
||||
global.File = require('undici').File;
|
||||
global.FormData = require('undici').FormData;
|
||||
}
|
||||
|
||||
function formatV8Stack(stack) {
|
||||
let v8StyleStack = '';
|
||||
if (stack) {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const [name] = stack[i];
|
||||
if (v8StyleStack !== '') {
|
||||
v8StyleStack += '\n';
|
||||
}
|
||||
v8StyleStack += ' in ' + name + ' (at **)';
|
||||
}
|
||||
}
|
||||
return v8StyleStack;
|
||||
}
|
||||
|
||||
function normalizeComponentInfo(debugInfo) {
|
||||
if (Array.isArray(debugInfo.stack)) {
|
||||
const {debugTask, debugStack, ...copy} = debugInfo;
|
||||
copy.stack = formatV8Stack(debugInfo.stack);
|
||||
if (debugInfo.owner) {
|
||||
copy.owner = normalizeComponentInfo(debugInfo.owner);
|
||||
}
|
||||
return copy;
|
||||
} else {
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebugInfo(obj) {
|
||||
const debugInfo = obj._debugInfo;
|
||||
if (debugInfo) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < debugInfo.length; i++) {
|
||||
copy.push(normalizeComponentInfo(debugInfo[i]));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return debugInfo;
|
||||
}
|
||||
|
||||
let act;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let ReactNoopFlightServer;
|
||||
let ReactNoopFlightClient;
|
||||
|
||||
describe('ReactFlight', () => {
|
||||
beforeEach(() => {
|
||||
// Mock performance.now for timing tests
|
||||
let time = 10;
|
||||
const now = jest.fn().mockImplementation(() => {
|
||||
return time++;
|
||||
});
|
||||
Object.defineProperty(performance, 'timeOrigin', {
|
||||
value: time,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(performance, 'now', {
|
||||
value: now,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
jest.resetModules();
|
||||
jest.mock('react', () => require('react/react.react-server'));
|
||||
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
|
||||
// This stores the state so we need to preserve it
|
||||
const flightModules = require('react-noop-renderer/flight-modules');
|
||||
jest.resetModules();
|
||||
__unmockReact();
|
||||
jest.mock('react-noop-renderer/flight-modules', () => flightModules);
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
|
||||
act = require('internal-test-utils').act;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableComponentPerformanceTrack
|
||||
it('can render deep but cut off JSX in debug info', async () => {
|
||||
function createDeepJSX(n) {
|
||||
if (n <= 0) {
|
||||
return null;
|
||||
}
|
||||
return <div>{createDeepJSX(n - 1)}</div>;
|
||||
}
|
||||
|
||||
function ServerComponent(props) {
|
||||
return <div>not using props</div>;
|
||||
}
|
||||
|
||||
const debugChannel = {onMessage(message) {}};
|
||||
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
{
|
||||
root: (
|
||||
<ServerComponent>
|
||||
{createDeepJSX(100) /* deper than objectLimit */}
|
||||
</ServerComponent>
|
||||
),
|
||||
},
|
||||
{debugChannel},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const rootModel = await ReactNoopFlightClient.read(transport, {
|
||||
debugChannel,
|
||||
});
|
||||
const root = rootModel.root;
|
||||
const children = getDebugInfo(root)[1].props.children;
|
||||
expect(children.type).toBe('div');
|
||||
expect(children.props.children.type).toBe('div');
|
||||
ReactNoop.render(root);
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
|
||||
});
|
||||
});
|
||||
@@ -5831,13 +5831,21 @@ export function attach(
|
||||
}
|
||||
|
||||
function getSourceForInstance(instance: DevToolsInstance): Source | null {
|
||||
const unresolvedSource = instance.source;
|
||||
let unresolvedSource = instance.source;
|
||||
if (unresolvedSource === null) {
|
||||
// We don't have any source yet. We can try again later in case an owned child mounts later.
|
||||
// TODO: We won't have any information here if the child is filtered.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (instance.kind === VIRTUAL_INSTANCE) {
|
||||
// We might have found one on the virtual instance.
|
||||
const debugLocation = instance.data.debugLocation;
|
||||
if (debugLocation != null) {
|
||||
unresolvedSource = debugLocation;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have the debug stack (the creation stack of the JSX) for any owned child of this
|
||||
// component, then at the bottom of that stack will be a stack frame that is somewhere within
|
||||
// the component's function body. Typically it would be the callsite of the JSX unless there's
|
||||
|
||||
@@ -6,7 +6,7 @@ export const markShellTime =
|
||||
export const clientRenderBoundary =
|
||||
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
|
||||
export const completeBoundary =
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
|
||||
'$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];null!==e.parentNode&&e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};\n$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
|
||||
export const completeBoundaryWithStyles =
|
||||
|
||||
@@ -34,11 +34,16 @@ export function revealCompletedBoundaries(batch) {
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const contentNode = batch[i + 1];
|
||||
// We can detach the content now.
|
||||
// Completions of boundaries within this contentNode will now find the boundary
|
||||
// in its designated place.
|
||||
contentNode.parentNode.removeChild(contentNode);
|
||||
|
||||
if (contentNode.parentNode === null) {
|
||||
// If the client has failed hydration we may have already deleted the streaming
|
||||
// segments. The server may also have emitted a complete instruction but cancelled
|
||||
// the segment. Regardless we can ignore this case.
|
||||
} else {
|
||||
// We can detach the content now.
|
||||
// Completions of boundaries within this contentNode will now find the boundary
|
||||
// in its designated place.
|
||||
contentNode.parentNode.removeChild(contentNode);
|
||||
}
|
||||
// Clear all the existing children. This is complicated because
|
||||
// there can be embedded Suspense boundaries in the fallback.
|
||||
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
|
||||
|
||||
@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
|
||||
undefined,
|
||||
'Markup',
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const flightResponse = createFlightResponse(
|
||||
null,
|
||||
|
||||
@@ -24,7 +24,7 @@ type Source = Array<Uint8Array>;
|
||||
|
||||
const decoderOptions = {stream: true};
|
||||
|
||||
const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
|
||||
const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
|
||||
createStringDecoder() {
|
||||
return new TextDecoder();
|
||||
},
|
||||
@@ -56,6 +56,8 @@ const {createResponse, processBinaryChunk, getRoot} = ReactFlightClient({
|
||||
|
||||
type ReadOptions = {|
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
debugChannel?: {onMessage: (message: string) => void},
|
||||
close?: boolean,
|
||||
|};
|
||||
|
||||
function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
@@ -70,10 +72,16 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
|
||||
options !== undefined ? options.findSourceMapURL : undefined,
|
||||
true,
|
||||
undefined,
|
||||
__DEV__ && options !== undefined && options.debugChannel !== undefined
|
||||
? options.debugChannel.onMessage
|
||||
: undefined,
|
||||
);
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
processBinaryChunk(response, source[i], 0);
|
||||
}
|
||||
if (options !== undefined && options.close) {
|
||||
close(response);
|
||||
}
|
||||
return getRoot(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ type Options = {
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
signal?: AbortSignal,
|
||||
debugChannel?: {onMessage?: (message: string) => void},
|
||||
onError?: (error: mixed) => void,
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
__DEV__ && options && options.debugChannel !== undefined,
|
||||
);
|
||||
const signal = options ? options.signal : undefined;
|
||||
if (signal) {
|
||||
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (__DEV__ && options && options.debugChannel !== undefined) {
|
||||
options.debugChannel.onMessage = message => {
|
||||
ReactNoopFlightServer.resolveDebugMessage(request, message);
|
||||
};
|
||||
}
|
||||
ReactNoopFlightServer.startWork(request);
|
||||
ReactNoopFlightServer.startFlowing(request, destination);
|
||||
return destination;
|
||||
|
||||
@@ -236,7 +236,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
pushTopLevelContextObject,
|
||||
invalidateContextProvider,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {
|
||||
getIsHydrating,
|
||||
enterHydrationState,
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
getUnmaskedContext,
|
||||
hasContextChanged,
|
||||
emptyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {readContext, checkIfContextChanged} from './ReactFiberNewContext';
|
||||
import {requestUpdateLane, scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
|
||||
import {
|
||||
@@ -695,7 +695,7 @@ function constructClassInstance(
|
||||
}
|
||||
|
||||
// Cache unmasked context so we can avoid recreating masked context unless necessary.
|
||||
// ReactFiberContext usually updates this cache but can't for newly-created instances.
|
||||
// ReactFiberLegacyContext usually updates this cache but can't for newly-created instances.
|
||||
if (isLegacyContextConsumer) {
|
||||
cacheContext(workInProgress, unmaskedContext, context);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {popProvider} from './ReactFiberNewContext';
|
||||
import {
|
||||
prepareToHydrateHostInstance,
|
||||
|
||||
@@ -79,7 +79,11 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const entry = debugInfo[i];
|
||||
if (typeof entry.name === 'string') {
|
||||
info += describeDebugInfoFrame(entry.name, entry.env);
|
||||
info += describeDebugInfoFrame(
|
||||
entry.name,
|
||||
entry.env,
|
||||
entry.debugLocation,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
|
||||
import {updateContainerSync} from './ReactFiberReconciler';
|
||||
import {emptyContextObject} from './ReactFiberContext';
|
||||
import {emptyContextObject} from './ReactFiberLegacyContext';
|
||||
import {SyncLane} from './ReactFiberLane';
|
||||
import {
|
||||
ClassComponent,
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
processChildContext,
|
||||
emptyContextObject,
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {createFiberRoot} from './ReactFiberRoot';
|
||||
import {isRootDehydrated} from './ReactFiberShellHydration';
|
||||
import {
|
||||
|
||||
33
packages/react-reconciler/src/ReactFiberStack.js
vendored
33
packages/react-reconciler/src/ReactFiberStack.js
vendored
@@ -27,10 +27,6 @@ function createCursor<T>(defaultValue: T): StackCursor<T> {
|
||||
};
|
||||
}
|
||||
|
||||
function isEmpty(): boolean {
|
||||
return index === -1;
|
||||
}
|
||||
|
||||
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
|
||||
if (index < 0) {
|
||||
if (__DEV__) {
|
||||
@@ -67,31 +63,4 @@ function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
|
||||
|
||||
cursor.current = value;
|
||||
}
|
||||
|
||||
function checkThatStackIsEmpty() {
|
||||
if (__DEV__) {
|
||||
if (index !== -1) {
|
||||
console.error(
|
||||
'Expected an empty stack. Something was not reset properly.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetStackAfterFatalErrorInDev() {
|
||||
if (__DEV__) {
|
||||
index = -1;
|
||||
valueStack.length = 0;
|
||||
fiberStack.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
createCursor,
|
||||
isEmpty,
|
||||
pop,
|
||||
push,
|
||||
// DEV only:
|
||||
checkThatStackIsEmpty,
|
||||
resetStackAfterFatalErrorInDev,
|
||||
};
|
||||
export {createCursor, pop, push};
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
isContextProvider as isLegacyContextProvider,
|
||||
popContext as popLegacyContext,
|
||||
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
||||
} from './ReactFiberContext';
|
||||
} from './ReactFiberLegacyContext';
|
||||
import {popProvider} from './ReactFiberNewContext';
|
||||
import {popCacheProvider} from './ReactFiberCacheComponent';
|
||||
import {transferActualDuration} from './ReactProfilerTimer';
|
||||
|
||||
@@ -357,7 +357,7 @@ import {
|
||||
flushSyncWorkOnLegacyRootsOnly,
|
||||
requestTransitionLane,
|
||||
} from './ReactFiberRootScheduler';
|
||||
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
|
||||
import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext';
|
||||
import {logUncaughtError} from './ReactFiberErrorLogger';
|
||||
import {
|
||||
deleteScheduledGesture,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -43,12 +44,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
export type Options = {
|
||||
moduleBaseURL?: string,
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
options && options.moduleBaseURL ? options.moduleBaseURL : '',
|
||||
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {
|
||||
@@ -27,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -50,6 +54,12 @@ export {
|
||||
registerClientReference,
|
||||
} from '../ReactFlightESMReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -86,6 +158,7 @@ function renderToPipeableStream(
|
||||
moduleBasePath: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
moduleBasePath,
|
||||
@@ -95,9 +168,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -126,11 +203,12 @@ function renderToPipeableStream(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeWritable(readable: any): Writable {
|
||||
// The current host config expects a Writable so we create
|
||||
// a fake writable for now to push into the Readable.
|
||||
return ({
|
||||
write(chunk) {
|
||||
write(chunk: string | Uint8Array) {
|
||||
return readable.push(chunk);
|
||||
},
|
||||
end() {
|
||||
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -287,8 +366,8 @@ function decodeReply<T>(
|
||||
export {
|
||||
renderToPipeableStream,
|
||||
prerenderToNodeStream,
|
||||
decodeReplyFromBusboy,
|
||||
decodeReply,
|
||||
decodeReplyFromBusboy,
|
||||
decodeAction,
|
||||
decodeFormState,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
*/
|
||||
|
||||
import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
@@ -76,6 +79,24 @@ export function createServerReference<A: Iterable<any>, T>(
|
||||
);
|
||||
}
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromStream(
|
||||
response: FlightResponse,
|
||||
stream: ReadableStream,
|
||||
@@ -104,6 +125,7 @@ function startReadingFromStream(
|
||||
}
|
||||
|
||||
export type Options = {
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
@@ -128,6 +150,12 @@ export function createFromReadableStream<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
startReadingFromStream(response, stream);
|
||||
return getRoot(response);
|
||||
@@ -152,6 +180,12 @@ export function createFromFetch<T>(
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
promiseForResponse.then(
|
||||
function (r) {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -24,6 +27,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -42,12 +47,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -57,10 +69,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -70,6 +127,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -83,6 +141,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -117,9 +178,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -144,6 +202,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
|
||||
import {
|
||||
preloadModule,
|
||||
@@ -26,6 +29,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -47,12 +52,19 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -62,10 +74,55 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -75,6 +132,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -88,6 +146,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -122,9 +183,6 @@ export function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -149,6 +207,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -20,6 +20,8 @@ import type {
|
||||
ServerReferenceId,
|
||||
} from '../client/ReactFlightClientConfigBundlerParcel';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -49,6 +53,7 @@ import {
|
||||
decodeAction as decodeActionImpl,
|
||||
decodeFormState as decodeFormStateImpl,
|
||||
} from 'react-server/src/ReactFlightActionServer';
|
||||
|
||||
import {
|
||||
preloadModule,
|
||||
requireModule,
|
||||
@@ -60,6 +65,12 @@ export {
|
||||
registerServerReference,
|
||||
} from '../ReactFlightParcelReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -79,7 +90,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -97,6 +170,7 @@ export function renderToPipeableStream(
|
||||
model: ReactClientValue,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -106,9 +180,13 @@ export function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -149,7 +227,7 @@ function createFakeWritableFromReadableStreamController(
|
||||
chunk = textEncoder.encode(chunk);
|
||||
}
|
||||
controller.enqueue(chunk);
|
||||
// in web streams there is no backpressure so we can alwas write more
|
||||
// in web streams there is no backpressure so we can always write more
|
||||
return true;
|
||||
},
|
||||
end() {
|
||||
@@ -167,13 +245,58 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
export function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
null,
|
||||
@@ -183,6 +306,7 @@ export function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -196,6 +320,9 @@ export function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -275,6 +402,7 @@ export function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -296,7 +424,6 @@ export function prerenderToNodeStream(
|
||||
|
||||
export function prerender(
|
||||
model: ReactClientValue,
|
||||
|
||||
options?: Options & {
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
@@ -338,6 +465,7 @@ export function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
|
||||
export type Options = {
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
null,
|
||||
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -19,6 +22,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -38,6 +43,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -54,11 +66,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -68,6 +125,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -81,6 +139,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -116,9 +177,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -143,6 +201,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -21,6 +24,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -43,6 +48,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -59,11 +71,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -73,6 +130,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -86,6 +144,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -121,9 +182,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -148,6 +206,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -29,6 +31,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -54,6 +58,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightTurbopackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -92,6 +164,7 @@ function renderToPipeableStream(
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -101,9 +174,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
turbopackMap: ClientManifest,
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
turbopackMap,
|
||||
@@ -178,6 +301,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -191,6 +315,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -334,6 +462,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
|
||||
import type {
|
||||
Response as FlightResponse,
|
||||
FindSourceMapURLCallback,
|
||||
DebugChannelCallback,
|
||||
} from 'react-client/src/ReactFlightClient';
|
||||
|
||||
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
|
||||
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
|
||||
|
||||
export type Options = {
|
||||
callServer?: CallServerCallback,
|
||||
debugChannel?: {writable?: WritableStream, ...},
|
||||
temporaryReferences?: TemporaryReferenceSet,
|
||||
findSourceMapURL?: FindSourceMapURLCallback,
|
||||
replayConsoleLogs?: boolean,
|
||||
environmentName?: string,
|
||||
};
|
||||
|
||||
function createDebugCallbackFromWritableStream(
|
||||
debugWritable: WritableStream,
|
||||
): DebugChannelCallback {
|
||||
const textEncoder = new TextEncoder();
|
||||
const writer = debugWritable.getWriter();
|
||||
return message => {
|
||||
if (message === '') {
|
||||
writer.close();
|
||||
} else {
|
||||
// Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
|
||||
// Therefore, we can't report errors from this write back to the Response object.
|
||||
if (__DEV__) {
|
||||
writer.write(textEncoder.encode(message + '\n')).catch(console.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createResponseFromOptions(options: void | Options) {
|
||||
return createResponse(
|
||||
null,
|
||||
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
|
||||
__DEV__ && options && options.environmentName
|
||||
? options.environmentName
|
||||
: undefined,
|
||||
__DEV__ &&
|
||||
options &&
|
||||
options.debugChannel !== undefined &&
|
||||
options.debugChannel.writable !== undefined
|
||||
? createDebugCallbackFromWritableStream(options.debugChannel.writable)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -19,6 +22,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -38,6 +43,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -54,11 +66,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -68,6 +125,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -81,6 +139,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -116,9 +177,6 @@ function prerender(
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
start: (controller): ?Promise<void> => {
|
||||
startWork(request);
|
||||
},
|
||||
pull: (controller): ?Promise<void> => {
|
||||
startFlowing(request, controller);
|
||||
},
|
||||
@@ -143,6 +201,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
|
||||
import type {
|
||||
Request,
|
||||
ReactClientValue,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
|
||||
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
|
||||
@@ -21,6 +24,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -43,6 +48,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
|
||||
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
|
||||
export type {TemporaryReferenceSet};
|
||||
|
||||
type Options = {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
identifierPrefix?: string,
|
||||
@@ -59,11 +71,56 @@ type Options = {
|
||||
onPostpone?: (reason: string) => void,
|
||||
};
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -73,6 +130,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -86,6 +144,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
type: 'bytes',
|
||||
@@ -145,6 +206,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
|
||||
import type {Writable} from 'stream';
|
||||
import type {Thenable} from 'shared/ReactTypes';
|
||||
|
||||
import type {Duplex} from 'stream';
|
||||
|
||||
import {Readable} from 'stream';
|
||||
|
||||
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
|
||||
@@ -29,6 +31,8 @@ import {
|
||||
startFlowing,
|
||||
stopFlowing,
|
||||
abort,
|
||||
resolveDebugMessage,
|
||||
closeDebugChannel,
|
||||
} from 'react-server/src/ReactFlightServer';
|
||||
|
||||
import {
|
||||
@@ -54,6 +58,12 @@ export {
|
||||
createClientModuleProxy,
|
||||
} from '../ReactFlightWebpackReferences';
|
||||
|
||||
import {
|
||||
createStringDecoder,
|
||||
readPartialStringChunk,
|
||||
readFinalStringChunk,
|
||||
} from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
|
||||
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
|
||||
|
||||
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
|
||||
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadable(
|
||||
request: Request,
|
||||
stream: Readable | WebSocket,
|
||||
): void {
|
||||
const stringDecoder = createStringDecoder();
|
||||
let lastWasPartial = false;
|
||||
let stringBuffer = '';
|
||||
function onData(chunk: string | Uint8Array) {
|
||||
if (typeof chunk === 'string') {
|
||||
if (lastWasPartial) {
|
||||
stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
|
||||
lastWasPartial = false;
|
||||
}
|
||||
stringBuffer += chunk;
|
||||
} else {
|
||||
const buffer: Uint8Array = (chunk: any);
|
||||
stringBuffer += readPartialStringChunk(stringDecoder, buffer);
|
||||
lastWasPartial = true;
|
||||
}
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
}
|
||||
function onError(error: mixed) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
function onClose() {
|
||||
closeDebugChannel(request);
|
||||
}
|
||||
if (
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.addEventListener === 'function' &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof stream.binaryType === 'string'
|
||||
) {
|
||||
const ws: WebSocket = (stream: any);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.addEventListener('message', event => {
|
||||
// $FlowFixMe
|
||||
onData(event.data);
|
||||
});
|
||||
ws.addEventListener('error', event => {
|
||||
// $FlowFixMe
|
||||
onError(event.error);
|
||||
});
|
||||
ws.addEventListener('close', onClose);
|
||||
} else {
|
||||
const readable: Readable = (stream: any);
|
||||
readable.on('data', onData);
|
||||
readable.on('error', onError);
|
||||
readable.on('end', onClose);
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
debugChannel?: Readable | Duplex | WebSocket,
|
||||
environmentName?: string | (() => string),
|
||||
filterStackFrame?: (url: string, functionName: string) => boolean,
|
||||
onError?: (error: mixed) => void,
|
||||
@@ -92,6 +164,7 @@ function renderToPipeableStream(
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options,
|
||||
): PipeableStream {
|
||||
const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -101,9 +174,13 @@ function renderToPipeableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannel !== undefined,
|
||||
);
|
||||
let hasStartedFlowing = false;
|
||||
startWork(request);
|
||||
if (debugChannel !== undefined) {
|
||||
startReadingFromDebugChannelReadable(request, debugChannel);
|
||||
}
|
||||
return {
|
||||
pipe<T: Writable>(destination: T): T {
|
||||
if (hasStartedFlowing) {
|
||||
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
|
||||
}: any);
|
||||
}
|
||||
|
||||
function startReadingFromDebugChannelReadableStream(
|
||||
request: Request,
|
||||
stream: ReadableStream,
|
||||
): void {
|
||||
const reader = stream.getReader();
|
||||
const stringDecoder = createStringDecoder();
|
||||
let stringBuffer = '';
|
||||
function progress({
|
||||
done,
|
||||
value,
|
||||
}: {
|
||||
done: boolean,
|
||||
value: ?any,
|
||||
...
|
||||
}): void | Promise<void> {
|
||||
const buffer: Uint8Array = (value: any);
|
||||
stringBuffer += done
|
||||
? readFinalStringChunk(stringDecoder, new Uint8Array(0))
|
||||
: readPartialStringChunk(stringDecoder, buffer);
|
||||
const messages = stringBuffer.split('\n');
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
resolveDebugMessage(request, messages[i]);
|
||||
}
|
||||
stringBuffer = messages[messages.length - 1];
|
||||
if (done) {
|
||||
closeDebugChannel(request);
|
||||
return;
|
||||
}
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(e: any) {
|
||||
abort(
|
||||
request,
|
||||
new Error('Lost connection to the Debug Channel.', {
|
||||
cause: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
reader.read().then(progress).catch(error);
|
||||
}
|
||||
|
||||
function renderToReadableStream(
|
||||
model: ReactClientValue,
|
||||
webpackMap: ClientManifest,
|
||||
options?: Options & {
|
||||
options?: Omit<Options, 'debugChannel'> & {
|
||||
debugChannel?: {readable?: ReadableStream, ...},
|
||||
signal?: AbortSignal,
|
||||
},
|
||||
): ReadableStream {
|
||||
const debugChannelReadable =
|
||||
__DEV__ && options && options.debugChannel
|
||||
? options.debugChannel.readable
|
||||
: undefined;
|
||||
const request = createRequest(
|
||||
model,
|
||||
webpackMap,
|
||||
@@ -178,6 +301,7 @@ function renderToReadableStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
debugChannelReadable !== undefined,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -191,6 +315,9 @@ function renderToReadableStream(
|
||||
signal.addEventListener('abort', listener);
|
||||
}
|
||||
}
|
||||
if (debugChannelReadable !== undefined) {
|
||||
startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
|
||||
}
|
||||
let writable: Writable;
|
||||
const stream = new ReadableStream(
|
||||
{
|
||||
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
@@ -334,6 +462,7 @@ function prerender(
|
||||
options ? options.temporaryReferences : undefined,
|
||||
__DEV__ && options ? options.environmentName : undefined,
|
||||
__DEV__ && options ? options.filterStackFrame : undefined,
|
||||
false,
|
||||
);
|
||||
if (options && options.signal) {
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -86,7 +86,7 @@ function describeComponentStackByType(
|
||||
}
|
||||
}
|
||||
if (typeof type.name === 'string') {
|
||||
return describeDebugInfoFrame(type.name, type.env);
|
||||
return describeDebugInfoFrame(type.name, type.env, type.debugLocation);
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
|
||||
7
packages/react-server/src/ReactFizzServer.js
vendored
7
packages/react-server/src/ReactFizzServer.js
vendored
@@ -3099,6 +3099,9 @@ function replayElement(
|
||||
if (task.node === currentNode) {
|
||||
// This same element suspended so we need to pop the replay we just added.
|
||||
task.replay = replay;
|
||||
} else {
|
||||
// We finished rendering this node, so now we can consume this slot.
|
||||
replayNodes.splice(i, 1);
|
||||
}
|
||||
throw x;
|
||||
}
|
||||
@@ -4127,6 +4130,8 @@ function renderNode(
|
||||
const segment = task.blockedSegment;
|
||||
if (segment === null) {
|
||||
// Replay
|
||||
task = ((task: any): ReplayTask); // Refined
|
||||
const previousReplaySet: ReplaySet = task.replay;
|
||||
try {
|
||||
return renderNodeDestructive(request, task, node, childIndex);
|
||||
} catch (thrownValue) {
|
||||
@@ -4166,6 +4171,7 @@ function renderNode(
|
||||
task.keyPath = previousKeyPath;
|
||||
task.treeContext = previousTreeContext;
|
||||
task.componentStack = previousComponentStack;
|
||||
task.replay = previousReplaySet;
|
||||
if (__DEV__) {
|
||||
task.debugTask = previousDebugTask;
|
||||
}
|
||||
@@ -4199,6 +4205,7 @@ function renderNode(
|
||||
task.keyPath = previousKeyPath;
|
||||
task.treeContext = previousTreeContext;
|
||||
task.componentStack = previousComponentStack;
|
||||
task.replay = previousReplaySet;
|
||||
if (__DEV__) {
|
||||
task.debugTask = previousDebugTask;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactDebugInfo, ReactComponentInfo} from 'shared/ReactTypes';
|
||||
import type {
|
||||
ReactDebugInfo,
|
||||
ReactComponentInfo,
|
||||
ReactStackTrace,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
export const IO_NODE = 0;
|
||||
export const PROMISE_NODE = 1;
|
||||
@@ -22,10 +26,10 @@ type PromiseWithDebugInfo = interface extends Promise<any> {
|
||||
export type IONode = {
|
||||
tag: 0,
|
||||
owner: null | ReactComponentInfo,
|
||||
stack: Error, // callsite that spawned the I/O
|
||||
debugInfo: null, // not used on I/O
|
||||
stack: ReactStackTrace, // callsite that spawned the I/O
|
||||
start: number, // start time when the first part of the I/O sequence started
|
||||
end: number, // we typically don't use this. only when there's no promise intermediate.
|
||||
promise: null, // not used on I/O
|
||||
awaited: null, // I/O is only blocked on external.
|
||||
previous: null | AwaitNode | UnresolvedAwaitNode, // the preceeding await that spawned this new work
|
||||
};
|
||||
@@ -33,10 +37,10 @@ export type IONode = {
|
||||
export type PromiseNode = {
|
||||
tag: 1,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
|
||||
stack: Error, // callsite that created the Promise
|
||||
stack: ReactStackTrace, // callsite that created the Promise
|
||||
start: number, // start time when the Promise was created
|
||||
end: number, // end time when the Promise was resolved.
|
||||
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
|
||||
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
|
||||
previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
|
||||
};
|
||||
@@ -44,10 +48,10 @@ export type PromiseNode = {
|
||||
export type AwaitNode = {
|
||||
tag: 2,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
|
||||
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
start: number, // when we started blocking. This might be later than the I/O started.
|
||||
end: number, // when we unblocked. This might be later than the I/O resolved if there's CPU time.
|
||||
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
|
||||
awaited: null | AsyncSequence, // the promise we were waiting on
|
||||
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
|
||||
};
|
||||
@@ -55,10 +59,10 @@ export type AwaitNode = {
|
||||
export type UnresolvedPromiseNode = {
|
||||
tag: 3,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
|
||||
stack: Error, // callsite that created the Promise
|
||||
stack: ReactStackTrace, // callsite that created the Promise
|
||||
start: number, // start time when the Promise was created
|
||||
end: -1.1, // set when we resolve.
|
||||
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
|
||||
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
|
||||
previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
|
||||
};
|
||||
@@ -66,10 +70,10 @@ export type UnresolvedPromiseNode = {
|
||||
export type UnresolvedAwaitNode = {
|
||||
tag: 4,
|
||||
owner: null | ReactComponentInfo,
|
||||
debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
|
||||
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
stack: ReactStackTrace, // callsite that awaited (using await, .then(), Promise.all(), ...)
|
||||
start: number, // when we started blocking. This might be later than the I/O started.
|
||||
end: -1.1, // set when we resolve.
|
||||
promise: WeakRef<PromiseWithDebugInfo>, // a reference to this Promise if still referenced
|
||||
awaited: null | AsyncSequence, // the promise we were waiting on
|
||||
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
|
||||
};
|
||||
|
||||
780
packages/react-server/src/ReactFlightServer.js
vendored
780
packages/react-server/src/ReactFlightServer.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@ import {
|
||||
import {resolveOwner} from './flight/ReactFlightCurrentOwner';
|
||||
import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
|
||||
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
||||
import {parseStackTrace} from './ReactFlightServerConfig';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const getAsyncId = AsyncResource.prototype.asyncId;
|
||||
@@ -33,6 +34,23 @@ const getAsyncId = AsyncResource.prototype.asyncId;
|
||||
const pendingOperations: Map<number, AsyncSequence> =
|
||||
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
|
||||
|
||||
// This is a weird one. This map, keeps a dependent Promise alive if the child Promise is still alive.
|
||||
// A PromiseNode/AwaitNode cannot hold a strong reference to its own Promise because then it'll never get
|
||||
// GC:ed. We only need it if a dependent AwaitNode points to it. We could put a reference in the Node
|
||||
// but that would require a GC pass between every Node that gets destroyed. I.e. the root gets destroy()
|
||||
// called on it and then that release it from the pendingOperations map which allows the next one to GC
|
||||
// and so on. By putting this relationship in a WeakMap this could be done as a single pass in the VM.
|
||||
// We don't actually ever have to read from this map since we have WeakRef reference to these Promises
|
||||
// if they're still alive. It's also optional information so we could just expose only if GC didn't run.
|
||||
const awaitedPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
|
||||
enableAsyncDebugInfo
|
||||
? new WeakMap()
|
||||
: (null: any);
|
||||
const previousPromise: WeakMap<Promise<any>, Promise<any>> = __DEV__ &&
|
||||
enableAsyncDebugInfo
|
||||
? new WeakMap()
|
||||
: (null: any);
|
||||
|
||||
// Keep the last resolved await as a workaround for async functions missing data.
|
||||
let lastRanAwait: null | AwaitNode = null;
|
||||
|
||||
@@ -44,12 +62,6 @@ function resolvePromiseOrAwaitNode(
|
||||
resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
|
||||
? PROMISE_NODE
|
||||
: AWAIT_NODE): any);
|
||||
// The Promise can be garbage collected after this so we should extract debugInfo first.
|
||||
const promise = unresolvedNode.debugInfo.deref();
|
||||
resolvedNode.debugInfo =
|
||||
promise === undefined || promise._debugInfo === undefined
|
||||
? null
|
||||
: promise._debugInfo;
|
||||
resolvedNode.end = endTime;
|
||||
return resolvedNode;
|
||||
}
|
||||
@@ -71,6 +83,14 @@ export function initAsyncDebugInfo(): void {
|
||||
const trigger = pendingOperations.get(triggerAsyncId);
|
||||
let node: AsyncSequence;
|
||||
if (type === 'PROMISE') {
|
||||
if (trigger !== undefined && trigger.promise !== null) {
|
||||
const triggerPromise = trigger.promise.deref();
|
||||
if (triggerPromise !== undefined) {
|
||||
// Keep the awaited Promise alive as long as the child is alive so we can
|
||||
// trace its value at the end.
|
||||
awaitedPromise.set(resource, triggerPromise);
|
||||
}
|
||||
}
|
||||
const currentAsyncId = executionAsyncId();
|
||||
if (currentAsyncId !== triggerAsyncId) {
|
||||
// When you call .then() on a native Promise, or await/Promise.all() a thenable,
|
||||
@@ -80,15 +100,23 @@ export function initAsyncDebugInfo(): void {
|
||||
return;
|
||||
}
|
||||
const current = pendingOperations.get(currentAsyncId);
|
||||
if (current !== undefined && current.promise !== null) {
|
||||
const currentPromise = current.promise.deref();
|
||||
if (currentPromise !== undefined) {
|
||||
// Keep the previous Promise alive as long as the child is alive so we can
|
||||
// trace its value at the end.
|
||||
previousPromise.set(resource, currentPromise);
|
||||
}
|
||||
}
|
||||
// If the thing we're waiting on is another Await we still track that sequence
|
||||
// so that we can later pick the best stack trace in user space.
|
||||
node = ({
|
||||
tag: UNRESOLVED_AWAIT_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: new WeakRef((resource: Promise<any>)),
|
||||
stack: new Error(),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // set when resolved.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
|
||||
previous: current === undefined ? null : current, // The path that led us here.
|
||||
}: UnresolvedAwaitNode);
|
||||
@@ -96,10 +124,10 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: UNRESOLVED_PROMISE_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: new WeakRef((resource: Promise<any>)),
|
||||
stack: new Error(),
|
||||
stack: parseStackTrace(new Error(), 5),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Set when we resolve.
|
||||
promise: new WeakRef((resource: Promise<any>)),
|
||||
awaited:
|
||||
trigger === undefined
|
||||
? null // It might get overridden when we resolve.
|
||||
@@ -117,10 +145,10 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: null,
|
||||
stack: new Error(), // This is only used if no native promises are used.
|
||||
stack: parseStackTrace(new Error(), 3), // This is only used if no native promises are used.
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
awaited: null,
|
||||
previous: null,
|
||||
}: IONode);
|
||||
@@ -132,10 +160,10 @@ export function initAsyncDebugInfo(): void {
|
||||
node = ({
|
||||
tag: IO_NODE,
|
||||
owner: resolveOwner(),
|
||||
debugInfo: null,
|
||||
stack: new Error(),
|
||||
stack: parseStackTrace(new Error(), 3),
|
||||
start: performance.now(),
|
||||
end: -1.1, // Only set when pinged.
|
||||
promise: null,
|
||||
awaited: null,
|
||||
previous: trigger,
|
||||
}: IONode);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
|
||||
import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
|
||||
|
||||
import {formatOwnerStack} from './ReactOwnerStackFrames';
|
||||
|
||||
let prefix;
|
||||
let suffix;
|
||||
export function describeBuiltInComponentFrame(name: string): string {
|
||||
@@ -38,7 +40,24 @@ export function describeBuiltInComponentFrame(name: string): string {
|
||||
return '\n' + prefix + name + suffix;
|
||||
}
|
||||
|
||||
export function describeDebugInfoFrame(name: string, env: ?string): string {
|
||||
export function describeDebugInfoFrame(
|
||||
name: string,
|
||||
env: ?string,
|
||||
location: ?Error,
|
||||
): string {
|
||||
if (location != null) {
|
||||
// If we have a location, it's the child's owner stack. Treat the bottom most frame as
|
||||
// the location of this function.
|
||||
const childStack = formatOwnerStack(location);
|
||||
const idx = childStack.lastIndexOf('\n');
|
||||
const lastLine = idx === -1 ? childStack : childStack.slice(idx + 1);
|
||||
if (lastLine.indexOf(name) !== -1) {
|
||||
// For async stacks it's possible we don't have the owner on it. As a precaution only
|
||||
// use this frame if it has the name of the function in it.
|
||||
return '\n' + lastLine;
|
||||
}
|
||||
}
|
||||
|
||||
return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,18 @@ function isObjectPrototype(object: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isGetter(object: any, name: string): boolean {
|
||||
const ObjectPrototype = Object.prototype;
|
||||
if (object === ObjectPrototype || object === null) {
|
||||
return false;
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
||||
if (descriptor === undefined) {
|
||||
return isGetter(getPrototypeOf(object), name);
|
||||
}
|
||||
return typeof descriptor.get === 'function';
|
||||
}
|
||||
|
||||
export function isSimpleObject(object: any): boolean {
|
||||
if (!isObjectPrototype(getPrototypeOf(object))) {
|
||||
return false;
|
||||
@@ -80,9 +92,8 @@ export function isSimpleObject(object: any): boolean {
|
||||
export function objectName(object: mixed): string {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const name = Object.prototype.toString.call(object);
|
||||
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
|
||||
return p0;
|
||||
});
|
||||
// Extract 'Object' from '[object Object]':
|
||||
return name.slice(8, name.length - 1);
|
||||
}
|
||||
|
||||
function describeKeyForErrorMessage(key: string): string {
|
||||
|
||||
@@ -209,6 +209,7 @@ export type ReactComponentInfo = {
|
||||
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
|
||||
+debugStack?: null | Error,
|
||||
+debugTask?: null | ConsoleTask,
|
||||
debugLocation?: null | Error,
|
||||
};
|
||||
|
||||
export type ReactEnvironmentInfo = {
|
||||
@@ -234,6 +235,7 @@ export type ReactIOInfo = {
|
||||
+name: string, // the name of the async function being called (e.g. "fetch")
|
||||
+start: number, // the start time
|
||||
+end: number, // the end time (this might be different from the time the await was unblocked)
|
||||
+value?: null | Promise<mixed>, // the Promise that was awaited if any, may be rejected
|
||||
+env?: string, // the environment where this I/O was spawned.
|
||||
+owner?: null | ReactComponentInfo,
|
||||
+stack?: null | ReactStackTrace,
|
||||
|
||||
@@ -26,3 +26,4 @@ export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
|
||||
export const enableLazyPublicInstanceInFabric = __VARIANT__;
|
||||
export const renameElementSymbol = __VARIANT__;
|
||||
export const enableFragmentRefs = __VARIANT__;
|
||||
export const enableComponentPerformanceTrack = __VARIANT__;
|
||||
|
||||
@@ -59,7 +59,6 @@ export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableReactTestRendererWarning = false;
|
||||
export const enableRetryLaneExpiration = false;
|
||||
export const enableSchedulingProfiler = __PROFILE__;
|
||||
export const enableComponentPerformanceTrack = false;
|
||||
export const enableScopeAPI = false;
|
||||
export const enableSuspenseAvoidThisFallback = false;
|
||||
export const enableSuspenseCallback = true;
|
||||
@@ -84,6 +83,8 @@ export const enableSrcObject = false;
|
||||
export const enableHydrationChangeEvent = true;
|
||||
export const enableDefaultTransitionIndicator = false;
|
||||
export const ownerStackLimit = 1e4;
|
||||
export const enableComponentPerformanceTrack: boolean =
|
||||
__PROFILE__ && dynamicFlags.enableComponentPerformanceTrack;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -548,5 +548,7 @@
|
||||
"560": "Cannot use a startGestureTransition() with a comment node root.",
|
||||
"561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.",
|
||||
"562": "The render was aborted due to a fatal error.",
|
||||
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
|
||||
"563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
|
||||
"564": "Unknown command. The debugChannel was not wired up properly.",
|
||||
"565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user