Compare commits
5 Commits
pr33625
...
mcp-sync-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f844195 | ||
|
|
c94e8b4461 | ||
|
|
6b04874535 | ||
|
|
1ba1485a65 | ||
|
|
2c1e4e4513 |
@@ -349,9 +349,11 @@ function codegenReactiveFunction(
|
||||
fn: ReactiveFunction,
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
cx.declare(place.identifier);
|
||||
if (param.kind === 'Identifier') {
|
||||
cx.temp.set(param.identifier.declarationId, null);
|
||||
} else {
|
||||
cx.temp.set(param.place.identifier.declarationId, null);
|
||||
}
|
||||
}
|
||||
|
||||
const params = fn.params.map(param => convertParameter(param));
|
||||
@@ -1181,7 +1183,7 @@ function codegenTerminal(
|
||||
? codegenPlaceToExpression(cx, case_.test)
|
||||
: null;
|
||||
const block = codegenBlock(cx, case_.block!);
|
||||
return t.switchCase(test, block.body.length === 0 ? [] : [block]);
|
||||
return t.switchCase(test, [block]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ export function extractScopeDeclarationsFromDestructuring(
|
||||
fn: ReactiveFunction,
|
||||
): void {
|
||||
const state = new State(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.declared.add(place.identifier.declarationId);
|
||||
}
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ function Component(props) {
|
||||
console.log(handlers.value);
|
||||
break bb0;
|
||||
}
|
||||
default:
|
||||
default: {
|
||||
}
|
||||
}
|
||||
|
||||
t0 = handlers;
|
||||
|
||||
@@ -67,7 +67,8 @@ function Component(props) {
|
||||
case "b": {
|
||||
break bb1;
|
||||
}
|
||||
case "c":
|
||||
case "c": {
|
||||
}
|
||||
default: {
|
||||
x = 6;
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({other, ...props}, ref) {
|
||||
[props, ref] = useIdentity([props, ref]);
|
||||
return <Stringify props={props} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0, ref) {
|
||||
const $ = _c(7);
|
||||
let props;
|
||||
if ($[0] !== t0) {
|
||||
let { other, ...t1 } = t0;
|
||||
props = t1;
|
||||
$[0] = t0;
|
||||
$[1] = props;
|
||||
} else {
|
||||
props = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] !== props || $[3] !== ref) {
|
||||
t1 = [props, ref];
|
||||
$[2] = props;
|
||||
$[3] = ref;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
[props, ref] = useIdentity(t1);
|
||||
let t2;
|
||||
if ($[5] !== props) {
|
||||
t2 = <Stringify props={props} />;
|
||||
$[5] = props;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: 0, b: "hello", children: <div>Hello</div> }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"props":{"a":0,"b":"hello","children":{"type":"div","key":null,"props":{"children":"Hello"},"_owner":"[[ cyclic ref *3 ]]","_store":{}}}}</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({other, ...props}, ref) {
|
||||
[props, ref] = useIdentity([props, ref]);
|
||||
return <Stringify props={props} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: 0, b: 'hello', children: <div>Hello</div>}],
|
||||
};
|
||||
@@ -50,8 +50,10 @@ function Component(props) {
|
||||
case 1: {
|
||||
break bb0;
|
||||
}
|
||||
case 2:
|
||||
default:
|
||||
case 2: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (props.cond2) {
|
||||
|
||||
@@ -41,7 +41,8 @@ function foo() {
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
default:
|
||||
default: {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,17 +43,22 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
function foo(x) {
|
||||
bb0: switch (x) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 0: {
|
||||
}
|
||||
case 1: {
|
||||
}
|
||||
case 2: {
|
||||
break bb0;
|
||||
}
|
||||
case 3: {
|
||||
break bb0;
|
||||
}
|
||||
case 4:
|
||||
case 5:
|
||||
default:
|
||||
case 4: {
|
||||
}
|
||||
case 5: {
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
TouchedViewDataAtPoint,
|
||||
ViewConfig,
|
||||
} from './ReactNativeTypes';
|
||||
import {create, diff} from './ReactNativeAttributePayloadFabric';
|
||||
import {dispatchEvent} from './ReactFabricEventEmitter';
|
||||
import {
|
||||
NoEventPriority,
|
||||
@@ -34,8 +35,6 @@ import {
|
||||
deepFreezeAndThrowOnMutationInDev,
|
||||
createPublicInstance,
|
||||
createPublicTextInstance,
|
||||
createAttributePayload,
|
||||
diffAttributePayloads,
|
||||
type PublicInstance as ReactNativePublicInstance,
|
||||
type PublicTextInstance,
|
||||
type PublicRootInstance,
|
||||
@@ -191,10 +190,7 @@ export function createInstance(
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayload = createAttributePayload(
|
||||
props,
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
const updatePayload = create(props, viewConfig.validAttributes);
|
||||
|
||||
const node = createNode(
|
||||
tag, // reactTag
|
||||
@@ -460,11 +456,7 @@ export function cloneInstance(
|
||||
newChildSet: ?ChildSet,
|
||||
): Instance {
|
||||
const viewConfig = instance.canonical.viewConfig;
|
||||
const updatePayload = diffAttributePayloads(
|
||||
oldProps,
|
||||
newProps,
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
|
||||
// TODO: If the event handlers have changed, we need to update the current props
|
||||
// in the commit phase but there is no host config hook to do it yet.
|
||||
// So instead we hack it by updating it in the render phase.
|
||||
@@ -513,7 +505,7 @@ export function cloneHiddenInstance(
|
||||
): Instance {
|
||||
const viewConfig = instance.canonical.viewConfig;
|
||||
const node = instance.node;
|
||||
const updatePayload = createAttributePayload(
|
||||
const updatePayload = create(
|
||||
{style: {display: 'none'}},
|
||||
viewConfig.validAttributes,
|
||||
);
|
||||
|
||||
514
packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js
vendored
Normal file
514
packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Modules provided by RN:
|
||||
import {
|
||||
deepDiffer,
|
||||
flattenStyle,
|
||||
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import isArray from 'shared/isArray';
|
||||
|
||||
import type {AttributeConfiguration} from './ReactNativeTypes';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
/**
|
||||
* Create a payload that contains all the updates between two sets of props.
|
||||
*
|
||||
* These helpers are all encapsulated into a single module, because they use
|
||||
* mutation as a performance optimization which leads to subtle shared
|
||||
* dependencies between the code paths. To avoid this mutable state leaking
|
||||
* across modules, I've kept them isolated to this module.
|
||||
*/
|
||||
|
||||
type NestedNode = Array<NestedNode> | Object;
|
||||
|
||||
// Tracks removed keys
|
||||
let removedKeys: {[string]: boolean} | null = null;
|
||||
let removedKeyCount = 0;
|
||||
|
||||
const deepDifferOptions = {
|
||||
unsafelyIgnoreFunctions: true,
|
||||
};
|
||||
|
||||
function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
|
||||
if (typeof nextProp !== 'object' || nextProp === null) {
|
||||
// Scalars have already been checked for equality
|
||||
return true;
|
||||
} else {
|
||||
// For objects and arrays, the default diffing algorithm is a deep compare
|
||||
return deepDiffer(prevProp, nextProp, deepDifferOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreDeletedValuesInNestedArray(
|
||||
updatePayload: Object,
|
||||
node: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
) {
|
||||
if (isArray(node)) {
|
||||
let i = node.length;
|
||||
while (i-- && removedKeyCount > 0) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
node[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
} else if (node && removedKeyCount > 0) {
|
||||
const obj = node;
|
||||
for (const propKey in removedKeys) {
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
if (!removedKeys[propKey]) {
|
||||
continue;
|
||||
}
|
||||
let nextProp = obj[propKey];
|
||||
if (nextProp === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (typeof nextProp === 'function') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = true;
|
||||
}
|
||||
if (typeof nextProp === 'undefined') {
|
||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
||||
nextProp = null;
|
||||
}
|
||||
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
removedKeys[propKey] = false;
|
||||
removedKeyCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function diffNestedArrayProperty(
|
||||
updatePayload: null | Object,
|
||||
prevArray: Array<NestedNode>,
|
||||
nextArray: Array<NestedNode>,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const minLength =
|
||||
prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
|
||||
let i;
|
||||
for (i = 0; i < minLength; i++) {
|
||||
// Diff any items in the array in the forward direction. Repeated keys
|
||||
// will be overwritten by later values.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
nextArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < prevArray.length; i++) {
|
||||
// Clear out all remaining properties.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevArray[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
for (; i < nextArray.length; i++) {
|
||||
// Add all remaining properties
|
||||
const nextProp = nextArray[i];
|
||||
if (!nextProp) {
|
||||
continue;
|
||||
}
|
||||
updatePayload = addNestedProperty(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
function diffNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
nextProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!updatePayload && prevProp === nextProp) {
|
||||
// If no properties have been added, then we can bail out quickly on object
|
||||
// equality.
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!prevProp || !nextProp) {
|
||||
if (nextProp) {
|
||||
return addNestedProperty(updatePayload, nextProp, validAttributes);
|
||||
}
|
||||
if (prevProp) {
|
||||
return clearNestedProperty(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!isArray(prevProp) && !isArray(nextProp)) {
|
||||
// Both are leaves, we can diff the leaves.
|
||||
return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
|
||||
}
|
||||
|
||||
if (isArray(prevProp) && isArray(nextProp)) {
|
||||
// Both are arrays, we can diff the arrays.
|
||||
return diffNestedArrayProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
if (isArray(prevProp)) {
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
flattenStyle(prevProp),
|
||||
nextProp,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
return diffProperties(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
flattenStyle(nextProp),
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* clearNestedProperty takes a single set of props and valid attributes. It
|
||||
* adds a null sentinel to the updatePayload, for each prop key.
|
||||
*/
|
||||
function clearNestedProperty(
|
||||
updatePayload: null | Object,
|
||||
prevProp: NestedNode,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
if (!prevProp) {
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
if (!isArray(prevProp)) {
|
||||
// Add each property of the leaf.
|
||||
return clearProperties(updatePayload, prevProp, validAttributes);
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevProp.length; i++) {
|
||||
// Add all the properties of the array.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp[i],
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* diffProperties takes two sets of props and a set of valid attributes
|
||||
* and write to updatePayload the values that changed or were deleted.
|
||||
* If no updatePayload is provided, a new one is created and returned if
|
||||
* anything changed.
|
||||
*/
|
||||
function diffProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
let attributeConfig;
|
||||
let nextProp;
|
||||
let prevProp;
|
||||
|
||||
for (const propKey in nextProps) {
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
nextProp = nextProps[propKey];
|
||||
|
||||
if (typeof nextProp === 'function') {
|
||||
const attributeConfigHasProcess =
|
||||
typeof attributeConfig === 'object' &&
|
||||
typeof attributeConfig.process === 'function';
|
||||
if (!attributeConfigHasProcess) {
|
||||
// functions are converted to booleans as markers that the associated
|
||||
// events should be sent from native.
|
||||
nextProp = (true: any);
|
||||
// If nextProp is not a function, then don't bother changing prevProp
|
||||
// since nextProp will win and go into the updatePayload regardless.
|
||||
if (typeof prevProp === 'function') {
|
||||
prevProp = (true: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit value of undefined is treated as a null because it overrides
|
||||
// any other preceding value.
|
||||
if (typeof nextProp === 'undefined') {
|
||||
nextProp = (null: any);
|
||||
if (typeof prevProp === 'undefined') {
|
||||
prevProp = (null: any);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedKeys) {
|
||||
removedKeys[propKey] = false;
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// Something else already triggered an update to this key because another
|
||||
// value diffed. Since we're now later in the nested arrays our value is
|
||||
// more important so we need to calculate it and override the existing
|
||||
// value. It doesn't matter if nothing changed, we'll set it anyway.
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
updatePayload[propKey] = nextProp;
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
updatePayload[propKey] = nextValue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prevProp === nextProp) {
|
||||
continue; // nothing changed
|
||||
}
|
||||
|
||||
// Pattern match on: attributeConfig
|
||||
if (typeof attributeConfig !== 'object') {
|
||||
// case: !Object is the default case
|
||||
if (defaultDiffer(prevProp, nextProp)) {
|
||||
// a normal leaf has changed
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextProp;
|
||||
}
|
||||
} else if (
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration
|
||||
const shouldUpdate =
|
||||
prevProp === undefined ||
|
||||
(typeof attributeConfig.diff === 'function'
|
||||
? attributeConfig.diff(prevProp, nextProp)
|
||||
: defaultDiffer(prevProp, nextProp));
|
||||
if (shouldUpdate) {
|
||||
const nextValue =
|
||||
typeof attributeConfig.process === 'function'
|
||||
? // $FlowFixMe[incompatible-use] found when upgrading Flow
|
||||
attributeConfig.process(nextProp)
|
||||
: nextProp;
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = nextValue;
|
||||
}
|
||||
} else {
|
||||
// default: fallthrough case when nested properties are defined
|
||||
removedKeys = null;
|
||||
removedKeyCount = 0;
|
||||
// We think that attributeConfig is not CustomAttributeConfiguration at
|
||||
// this point so we assume it must be AttributeConfiguration.
|
||||
updatePayload = diffNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
if (removedKeyCount > 0 && updatePayload) {
|
||||
restoreDeletedValuesInNestedArray(
|
||||
updatePayload,
|
||||
nextProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
removedKeys = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also iterate through all the previous props to catch any that have been
|
||||
// removed and make sure native gets the signal so it can reset them to the
|
||||
// default.
|
||||
for (const propKey in prevProps) {
|
||||
if (nextProps[propKey] !== undefined) {
|
||||
continue; // we've already covered this key in the previous pass
|
||||
}
|
||||
attributeConfig = validAttributes[propKey];
|
||||
if (!attributeConfig) {
|
||||
continue; // not a valid native prop
|
||||
}
|
||||
|
||||
if (updatePayload && updatePayload[propKey] !== undefined) {
|
||||
// This was already updated to a diff result earlier.
|
||||
continue;
|
||||
}
|
||||
|
||||
prevProp = prevProps[propKey];
|
||||
if (prevProp === undefined) {
|
||||
continue; // was already empty anyway
|
||||
}
|
||||
// Pattern match on: attributeConfig
|
||||
if (
|
||||
typeof attributeConfig !== 'object' ||
|
||||
typeof attributeConfig.diff === 'function' ||
|
||||
typeof attributeConfig.process === 'function'
|
||||
) {
|
||||
// case: CustomAttributeConfiguration | !Object
|
||||
// Flag the leaf property for removal by sending a sentinel.
|
||||
(updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
|
||||
propKey
|
||||
] = null;
|
||||
if (!removedKeys) {
|
||||
removedKeys = ({}: {[string]: boolean});
|
||||
}
|
||||
if (!removedKeys[propKey]) {
|
||||
removedKeys[propKey] = true;
|
||||
removedKeyCount++;
|
||||
}
|
||||
} else {
|
||||
// default:
|
||||
// This is a nested attribute configuration where all the properties
|
||||
// were removed so we need to go through and clear out all of them.
|
||||
updatePayload = clearNestedProperty(
|
||||
updatePayload,
|
||||
prevProp,
|
||||
((attributeConfig: any): AttributeConfiguration),
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatePayload;
|
||||
}
|
||||
|
||||
function addNestedProperty(
|
||||
payload: null | Object,
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
// Flatten nested style props.
|
||||
if (isArray(props)) {
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
payload = addNestedProperty(payload, props[i], validAttributes);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
for (const propKey in props) {
|
||||
const prop = props[propKey];
|
||||
|
||||
const attributeConfig = ((validAttributes[
|
||||
propKey
|
||||
]: any): AttributeConfiguration);
|
||||
|
||||
if (attributeConfig == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newValue;
|
||||
|
||||
if (prop === undefined) {
|
||||
// Discard the prop if it was previously defined.
|
||||
if (payload && payload[propKey] !== undefined) {
|
||||
newValue = null;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else if (typeof attributeConfig === 'object') {
|
||||
if (typeof attributeConfig.process === 'function') {
|
||||
// An atomic prop with custom processing.
|
||||
newValue = attributeConfig.process(prop);
|
||||
} else if (typeof attributeConfig.diff === 'function') {
|
||||
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
|
||||
newValue = prop;
|
||||
}
|
||||
} else {
|
||||
if (typeof prop === 'function') {
|
||||
// A function prop. It represents an event handler. Pass it to native as 'true'.
|
||||
newValue = true;
|
||||
} else {
|
||||
// An atomic prop. Doesn't need to be flattened.
|
||||
newValue = prop;
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue !== undefined) {
|
||||
if (!payload) {
|
||||
payload = ({}: {[string]: $FlowFixMe});
|
||||
}
|
||||
payload[propKey] = newValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
payload = addNestedProperty(payload, prop, attributeConfig);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* clearProperties clears all the previous props by adding a null sentinel
|
||||
* to the payload for each valid key.
|
||||
*/
|
||||
function clearProperties(
|
||||
updatePayload: null | Object,
|
||||
prevProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
|
||||
}
|
||||
|
||||
export function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return addNestedProperty(null, props, validAttributes);
|
||||
}
|
||||
|
||||
export function diff(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
return diffProperties(
|
||||
null, // updatePayload
|
||||
prevProps,
|
||||
nextProps,
|
||||
validAttributes,
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,15 @@ export type AttributeType<T, V> =
|
||||
export type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>;
|
||||
|
||||
export type AttributeConfiguration = $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
style: $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
...
|
||||
}>,
|
||||
...
|
||||
}>;
|
||||
|
||||
export type PartialAttributeConfiguration = $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
style?: $ReadOnly<{
|
||||
[propName: string]: AnyAttributeType,
|
||||
@@ -74,7 +83,7 @@ export type PartialViewConfig = $ReadOnly<{
|
||||
directEventTypes?: ViewConfig['directEventTypes'],
|
||||
supportsRawText?: boolean,
|
||||
uiViewClassName: string,
|
||||
validAttributes?: AttributeConfiguration,
|
||||
validAttributes?: PartialAttributeConfiguration,
|
||||
}>;
|
||||
|
||||
type InspectorDataProps = $ReadOnly<{
|
||||
|
||||
@@ -59,10 +59,4 @@ module.exports = {
|
||||
get createPublicRootInstance() {
|
||||
return require('./createPublicRootInstance').default;
|
||||
},
|
||||
get createAttributePayload() {
|
||||
return require('./createAttributePayload').default;
|
||||
},
|
||||
get diffAttributePayloads() {
|
||||
return require('./diffAttributePayloads').default;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
|
||||
|
||||
export default function create(
|
||||
props: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const {children, ...propsToPass} = props;
|
||||
return propsToPass;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict
|
||||
*/
|
||||
|
||||
import type {AttributeConfiguration} from '../../../../ReactNativeTypes';
|
||||
|
||||
import deepDiffer from './deepDiffer';
|
||||
|
||||
export default function diff(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: AttributeConfiguration,
|
||||
): null | Object {
|
||||
const {children: _prevChildren, ...prevPropsPassed} = prevProps;
|
||||
const {children: _nextChildren, ...nextPropsToPass} = nextProps;
|
||||
return deepDiffer(prevPropsPassed, nextPropsToPass) ? nextPropsToPass : null;
|
||||
}
|
||||
@@ -184,10 +184,6 @@ describe('ReactFabric', () => {
|
||||
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
|
||||
).not.toBeCalled();
|
||||
|
||||
jest
|
||||
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
|
||||
.mockReturnValue({bar: 'b'});
|
||||
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<Text foo="a" bar="b">
|
||||
@@ -207,9 +203,6 @@ describe('ReactFabric', () => {
|
||||
RCTText {"foo":"a","bar":"b"}
|
||||
RCTRawText {"text":"1"}`);
|
||||
|
||||
jest
|
||||
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
|
||||
.mockReturnValue({foo: 'b'});
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<Text foo="b" bar="b">
|
||||
@@ -619,7 +612,7 @@ describe('ReactFabric', () => {
|
||||
ReactFabric.render(<Component chars={before} />, 11, null, true);
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView {}
|
||||
RCTView null
|
||||
RCTView {"title":"a"}
|
||||
RCTView {"title":"b"}
|
||||
RCTView {"title":"c"}
|
||||
@@ -645,7 +638,7 @@ describe('ReactFabric', () => {
|
||||
ReactFabric.render(<Component chars={after} />, 11, null, true);
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView {}
|
||||
RCTView null
|
||||
RCTView {"title":"m"}
|
||||
RCTView {"title":"x"}
|
||||
RCTView {"title":"h"}
|
||||
@@ -707,8 +700,8 @@ describe('ReactFabric', () => {
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
|
||||
`11
|
||||
RCTView {}
|
||||
RCTView {}
|
||||
RCTView null
|
||||
RCTView null
|
||||
RCTView {"title":"a"}
|
||||
RCTView {"title":"b"}
|
||||
RCTView {"title":"c"}
|
||||
@@ -739,8 +732,8 @@ describe('ReactFabric', () => {
|
||||
});
|
||||
});
|
||||
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
|
||||
RCTView {}
|
||||
RCTView {}
|
||||
RCTView null
|
||||
RCTView null
|
||||
RCTView {"title":"m"}
|
||||
RCTView {"title":"x"}
|
||||
RCTView {"title":"h"}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jest-environment node
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {diff, create} = require('../ReactNativeAttributePayloadFabric');
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.create', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with complex example', () => {
|
||||
const validAttributes = {
|
||||
style: {
|
||||
position: true,
|
||||
zIndex: true,
|
||||
flexGrow: true,
|
||||
flexShrink: true,
|
||||
flexDirection: true,
|
||||
overflow: true,
|
||||
backgroundColor: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
style: [
|
||||
{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
[
|
||||
{position: 'relative', zIndex: 2},
|
||||
{flexGrow: 0},
|
||||
{backgroundColor: 'red'},
|
||||
],
|
||||
],
|
||||
},
|
||||
validAttributes,
|
||||
),
|
||||
).toEqual({
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
overflow: 'scroll',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
backgroundColor: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('should nullify previously defined style prop that is subsequently set to null or undefined', () => {
|
||||
expect(
|
||||
create({style: [{a: 0}, {a: undefined}]}, {style: {a: true}}),
|
||||
).toEqual({a: null});
|
||||
expect(create({style: [{a: 0}, {a: null}]}, {style: {a: true}})).toEqual({
|
||||
a: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore non-style fields that are set to undefined', () => {
|
||||
expect(create({}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined}, {a: true})).toEqual(null);
|
||||
expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual(
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true}),
|
||||
).toEqual(null);
|
||||
expect(
|
||||
create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}),
|
||||
).toEqual({c: 1});
|
||||
expect(
|
||||
create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}),
|
||||
).toEqual({a: 1, c: 2});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(create({b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute', () => {
|
||||
const diffA = jest.fn();
|
||||
expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should use the process attribute', () => {
|
||||
const processA = jest.fn(a => a + 1);
|
||||
expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3});
|
||||
expect(processA).toBeCalledWith(2);
|
||||
});
|
||||
|
||||
it('should use the process attribute for functions as well', () => {
|
||||
const process = x => x;
|
||||
const nextFunction = () => {};
|
||||
expect(create({a: nextFunction}, {a: {process}})).toEqual({
|
||||
a: nextFunction,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
|
||||
expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
|
||||
b: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
expect(create({}, validStyleAttribute)).toEqual(null);
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
expect(
|
||||
create(
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => {
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({
|
||||
a: [{foo: 1}, {bar: 2}],
|
||||
});
|
||||
expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({
|
||||
a: {foo: 1, bar: 2},
|
||||
});
|
||||
expect(
|
||||
create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}),
|
||||
).toEqual({a: [{foo: 1}, {bar: 2}]});
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({
|
||||
foo: null, // this should ideally be null.
|
||||
});
|
||||
expect(
|
||||
create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes),
|
||||
).toEqual({
|
||||
foo: 5,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
expect(
|
||||
create(
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
b: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true},
|
||||
),
|
||||
).toEqual({a: true, b: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReactNativeAttributePayloadFabric.diff', () => {
|
||||
it('should work with simple example', () => {
|
||||
expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({
|
||||
a: null,
|
||||
b: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip fields that are equal', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
|
||||
{a: true, b: true, c: true, d: true, e: true, f: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should remove fields', () => {
|
||||
expect(diff({a: 1}, {}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should remove fields that are set to undefined', () => {
|
||||
expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null});
|
||||
});
|
||||
|
||||
it('should ignore invalid fields', () => {
|
||||
expect(diff({a: 1}, {b: 2}, {})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should use the diff attribute', () => {
|
||||
const diffA = jest.fn((a, b) => true);
|
||||
const diffB = jest.fn((a, b) => false);
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: [3]},
|
||||
{a: [2], b: [4]},
|
||||
{a: {diff: diffA}, b: {diff: diffB}},
|
||||
),
|
||||
).toEqual({a: [2]});
|
||||
expect(diffA).toBeCalledWith([1], [2]);
|
||||
expect(diffB).toBeCalledWith([3], [4]);
|
||||
});
|
||||
|
||||
it('should not use the diff attribute on addition/removal', () => {
|
||||
const diffA = jest.fn();
|
||||
const diffB = jest.fn();
|
||||
expect(
|
||||
diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}),
|
||||
).toEqual({a: null, b: [2]});
|
||||
expect(diffA).not.toBeCalled();
|
||||
expect(diffB).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do deep diffs of Objects by default', () => {
|
||||
expect(
|
||||
diff(
|
||||
{a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}},
|
||||
{a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: [2], c: {k: [4, 5]}});
|
||||
});
|
||||
|
||||
it('should work with undefined styles', () => {
|
||||
expect(
|
||||
diff(
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: undefined},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: null});
|
||||
expect(
|
||||
diff(
|
||||
{style: undefined},
|
||||
{style: {a: '#ffffff', b: 1}},
|
||||
{style: {b: true}},
|
||||
),
|
||||
).toEqual({b: 1});
|
||||
expect(
|
||||
diff({style: undefined}, {style: undefined}, {style: {b: true}}),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should work with empty styles', () => {
|
||||
expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null});
|
||||
expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1});
|
||||
expect(diff({}, {}, {a: true, b: true})).toEqual(null);
|
||||
});
|
||||
|
||||
it('should flatten nested styles and predefined styles', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
|
||||
expect(
|
||||
diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute),
|
||||
).toEqual({foo: null, bar: null});
|
||||
|
||||
const barStyle = {
|
||||
bar: 3,
|
||||
};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{},
|
||||
{someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 2, bar: 3});
|
||||
});
|
||||
|
||||
it('should reset a value to a previous if it is removed', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 1, bar: 2});
|
||||
});
|
||||
|
||||
it('should not clear removed props if they are still in another slot', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 3}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1, bar: 1}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({bar: 2, foo: 1});
|
||||
});
|
||||
|
||||
it('should clear a prop if a later style is explicit null/undefined', () => {
|
||||
const validStyleAttribute = {someStyle: {foo: true, bar: true}};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{}, {foo: 3, bar: 2}]},
|
||||
{someStyle: [{foo: 1}, {bar: 2, foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 3}, {foo: null, bar: 2}]},
|
||||
{someStyle: [{foo: null}, {bar: 2}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null});
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: null}]},
|
||||
{someStyle: [{foo: 2}, {foo: null}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
|
||||
// Test the same case with object equality because an early bailout doesn't
|
||||
// work in this case.
|
||||
const fooObj = {foo: 3};
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, fooObj]},
|
||||
{someStyle: [{foo: 2}, fooObj]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: 3}); // this should ideally be null. heuristic.
|
||||
|
||||
expect(
|
||||
diff(
|
||||
{someStyle: [{foo: 1}, {foo: 3}]},
|
||||
{someStyle: [{foo: 2}, {foo: undefined}]},
|
||||
validStyleAttribute,
|
||||
),
|
||||
).toEqual({foo: null}); // this should ideally be null. heuristic.
|
||||
});
|
||||
|
||||
it('handles attributes defined multiple times', () => {
|
||||
const validAttributes = {foo: true, style: {foo: true}};
|
||||
expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({
|
||||
foo: 2,
|
||||
});
|
||||
expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({
|
||||
foo: 4,
|
||||
});
|
||||
});
|
||||
|
||||
// Function properties are just markers to native that events should be sent.
|
||||
it('should convert functions to booleans', () => {
|
||||
// Note that if the property changes from one function to another, we don't
|
||||
// need to send an update.
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
b: function () {
|
||||
return 2;
|
||||
},
|
||||
c: 3,
|
||||
},
|
||||
{
|
||||
b: function () {
|
||||
return 9;
|
||||
},
|
||||
c: function () {
|
||||
return 3;
|
||||
},
|
||||
},
|
||||
{a: true, b: true, c: true},
|
||||
),
|
||||
).toEqual({a: null, c: true});
|
||||
});
|
||||
|
||||
it('should skip changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
{a: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should skip deeply-nested changed functions', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wrapper: {
|
||||
a: function () {
|
||||
return 9;
|
||||
},
|
||||
},
|
||||
},
|
||||
{wrapper: true},
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should use the process function config when prop is a function', () => {
|
||||
const process = jest.fn(a => a);
|
||||
const nextFunction = function () {};
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
a: function () {},
|
||||
},
|
||||
{
|
||||
a: nextFunction,
|
||||
},
|
||||
{a: {process}},
|
||||
),
|
||||
).toEqual({a: nextFunction});
|
||||
expect(process).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes';
|
||||
import type {ReactComponentInfo} from 'shared/ReactTypes';
|
||||
import type {LazyComponent} from 'react/src/ReactLazy';
|
||||
|
||||
import {
|
||||
@@ -37,8 +37,7 @@ export type ComponentStackNode = {
|
||||
| string
|
||||
| Function
|
||||
| LazyComponent<any, any>
|
||||
| ReactComponentInfo
|
||||
| ReactAsyncInfo,
|
||||
| ReactComponentInfo,
|
||||
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
|
||||
stack?: null | string | Error, // DEV only
|
||||
};
|
||||
|
||||
102
packages/react-server/src/ReactFizzServer.js
vendored
102
packages/react-server/src/ReactFizzServer.js
vendored
@@ -21,7 +21,6 @@ import type {
|
||||
ReactFormState,
|
||||
ReactComponentInfo,
|
||||
ReactDebugInfo,
|
||||
ReactAsyncInfo,
|
||||
ViewTransitionProps,
|
||||
ActivityProps,
|
||||
SuspenseProps,
|
||||
@@ -182,7 +181,6 @@ import {
|
||||
enableAsyncIterableChildren,
|
||||
enableViewTransition,
|
||||
enableFizzBlockingRender,
|
||||
enableAsyncDebugInfo,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import assign from 'shared/assign';
|
||||
@@ -987,45 +985,6 @@ function getStackFromNode(stackNode: ComponentStackNode): string {
|
||||
return getStackByComponentStackNode(stackNode);
|
||||
}
|
||||
|
||||
function pushHaltedAwaitOnComponentStack(
|
||||
task: Task,
|
||||
debugInfo: void | null | ReactDebugInfo,
|
||||
): void {
|
||||
if (!__DEV__) {
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
throw new Error(
|
||||
'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
if (debugInfo != null) {
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.name === 'string') {
|
||||
// This is a Server Component. Any awaits in previous Server Components already resolved.
|
||||
break;
|
||||
}
|
||||
if (typeof info.time === 'number') {
|
||||
// This had an end time. Any awaits before this must have already resolved.
|
||||
break;
|
||||
}
|
||||
if (info.awaited != null) {
|
||||
const asyncInfo: ReactAsyncInfo = (info: any);
|
||||
const bestStack =
|
||||
asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;
|
||||
if (bestStack.debugStack !== undefined) {
|
||||
task.componentStack = {
|
||||
parent: task.componentStack,
|
||||
type: asyncInfo,
|
||||
owner: bestStack.owner,
|
||||
stack: bestStack.debugStack,
|
||||
};
|
||||
task.debugTask = (bestStack.debugTask: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pushServerComponentStack(
|
||||
task: Task,
|
||||
debugInfo: void | null | ReactDebugInfo,
|
||||
@@ -4653,20 +4612,6 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
}
|
||||
|
||||
const errorInfo = getThrownInfo(task.componentStack);
|
||||
if (__DEV__ && enableAsyncDebugInfo) {
|
||||
// If the task is not rendering, then this is an async abort. Conceptually it's as if
|
||||
// the abort happened inside the async gap. The abort reason's stack frame won't have that
|
||||
// on the stack so instead we use the owner stack and debug task of any halted async debug info.
|
||||
const node: any = task.node;
|
||||
if (node !== null && typeof node === 'object') {
|
||||
// Push a fake component stack frame that represents the await.
|
||||
pushHaltedAwaitOnComponentStack(task, node._debugInfo);
|
||||
if (task.thenableState !== null) {
|
||||
// TODO: If we were stalled inside use() of a Client Component then we should
|
||||
// rerender to get the stack trace from the use() call.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (boundary === null) {
|
||||
if (request.status !== CLOSING && request.status !== CLOSED) {
|
||||
@@ -4686,12 +4631,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
if (trackedPostpones !== null && segment !== null) {
|
||||
// We are prerendering. We don't want to fatal when the shell postpones
|
||||
// we just need to mark it as postponed.
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
@@ -4699,8 +4639,8 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
'The render was aborted with postpone when the shell is incomplete. Reason: ' +
|
||||
postponeInstance.message,
|
||||
);
|
||||
logRecoverableError(request, fatal, errorInfo, task.debugTask);
|
||||
fatalError(request, fatal, errorInfo, task.debugTask);
|
||||
logRecoverableError(request, fatal, errorInfo, null);
|
||||
fatalError(request, fatal, errorInfo, null);
|
||||
}
|
||||
} else if (
|
||||
enableHalt &&
|
||||
@@ -4710,12 +4650,12 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
const trackedPostpones = request.trackedPostpones;
|
||||
// We are aborting a prerender and must treat the shell as halted
|
||||
// We log the error but we still resolve the prerender
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
finishedTask(request, null, task.row, segment);
|
||||
} else {
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
fatalError(request, error, errorInfo, task.debugTask);
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
fatalError(request, error, errorInfo, null);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
@@ -4732,12 +4672,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
// TODO: Figure out a better signal than a magic digest value.
|
||||
errorDigest = 'POSTPONE';
|
||||
} else {
|
||||
@@ -4775,16 +4710,11 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
} else {
|
||||
// We are aborting a prerender and must halt this boundary.
|
||||
// We treat this like other postpones during prerendering
|
||||
logRecoverableError(request, error, errorInfo, task.debugTask);
|
||||
logRecoverableError(request, error, errorInfo, null);
|
||||
}
|
||||
trackPostpone(request, trackedPostpones, task, segment);
|
||||
// If this boundary was still pending then we haven't already cancelled its fallbacks.
|
||||
@@ -4807,12 +4737,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
error.$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
const postponeInstance: Postpone = (error: any);
|
||||
logPostpone(
|
||||
request,
|
||||
postponeInstance.message,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
logPostpone(request, postponeInstance.message, errorInfo, null);
|
||||
if (request.trackedPostpones !== null && segment !== null) {
|
||||
trackPostpone(request, request.trackedPostpones, task, segment);
|
||||
finishedTask(request, task.blockedBoundary, task.row, segment);
|
||||
@@ -4828,12 +4753,7 @@ function abortTask(task: Task, request: Request, error: mixed): void {
|
||||
// TODO: Figure out a better signal than a magic digest value.
|
||||
errorDigest = 'POSTPONE';
|
||||
} else {
|
||||
errorDigest = logRecoverableError(
|
||||
request,
|
||||
error,
|
||||
errorInfo,
|
||||
task.debugTask,
|
||||
);
|
||||
errorDigest = logRecoverableError(request, error, errorInfo, null);
|
||||
}
|
||||
boundary.status = CLIENT_RENDERED;
|
||||
encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true);
|
||||
|
||||
175
packages/react-server/src/ReactFlightServer.js
vendored
175
packages/react-server/src/ReactFlightServer.js
vendored
@@ -433,6 +433,7 @@ export type Request = {
|
||||
nextChunkId: number,
|
||||
pendingChunks: number,
|
||||
hints: Hints,
|
||||
abortListeners: Set<(reason: mixed) => void>,
|
||||
abortableTasks: Set<Task>,
|
||||
pingedTasks: Array<Task>,
|
||||
completedImportChunks: Array<Chunk>,
|
||||
@@ -546,6 +547,7 @@ function RequestInstance(
|
||||
this.nextChunkId = 0;
|
||||
this.pendingChunks = 0;
|
||||
this.hints = hints;
|
||||
this.abortListeners = new Set();
|
||||
this.abortableTasks = abortSet;
|
||||
this.pingedTasks = pingedTasks;
|
||||
this.completedImportChunks = ([]: Array<Chunk>);
|
||||
@@ -837,11 +839,13 @@ function serializeThenable(
|
||||
if (request.status === ABORTING) {
|
||||
// We can no longer accept any resolved values
|
||||
request.abortableTasks.delete(newTask);
|
||||
newTask.status = ABORTED;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(newTask, request);
|
||||
request.pendingChunks--;
|
||||
} else {
|
||||
const errorId: number = (request.fatalError: any);
|
||||
abortTask(newTask, request, errorId);
|
||||
const model = stringify(serializeByValueID(errorId));
|
||||
emitModelChunk(request, newTask.id, model);
|
||||
}
|
||||
return newTask.id;
|
||||
}
|
||||
@@ -932,26 +936,29 @@ function serializeReadableStream(
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
|
||||
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
|
||||
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (supportsBYOB ? 'r' : 'R') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
|
||||
// There's a race condition between when the stream is aborted and when the promise
|
||||
// resolves so we track whether we already aborted it to avoid writing twice.
|
||||
let aborted = false;
|
||||
function progress(entry: {done: boolean, value: ReactClientValue, ...}) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.done) {
|
||||
streamTask.status = COMPLETED;
|
||||
const endStreamRow = streamTask.id.toString(16) + ':C\n';
|
||||
request.completedRegularChunks.push(stringToChunk(endStreamRow));
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.cacheController.signal.removeEventListener('abort', abortStream);
|
||||
enqueueFlush(request);
|
||||
request.abortListeners.delete(abortStream);
|
||||
callOnAllReadyIfReady(request);
|
||||
aborted = true;
|
||||
} else {
|
||||
try {
|
||||
streamTask.model = entry.value;
|
||||
@@ -965,28 +972,26 @@ function serializeReadableStream(
|
||||
}
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
request.cacheController.signal.removeEventListener('abort', abortStream);
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortStream);
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
function abortStream() {
|
||||
if (streamTask.status !== PENDING) {
|
||||
function abortStream(reason: mixed) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortStream);
|
||||
const reason = signal.reason;
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortStream);
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.pendingChunks--;
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
@@ -994,7 +999,7 @@ function serializeReadableStream(
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
|
||||
request.cacheController.signal.addEventListener('abort', abortStream);
|
||||
request.abortListeners.add(abortStream);
|
||||
reader.read().then(progress, error);
|
||||
return serializeByValueID(streamTask.id);
|
||||
}
|
||||
@@ -1023,9 +1028,10 @@ function serializeAsyncIterable(
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
|
||||
request.pendingChunks++; // The task represents the Start row. This adds a Stop row.
|
||||
|
||||
// The task represents the Stop row. This adds a Start row.
|
||||
request.pendingChunks++;
|
||||
const startStreamRow =
|
||||
streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
|
||||
request.completedRegularChunks.push(stringToChunk(startStreamRow));
|
||||
@@ -1037,17 +1043,19 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
|
||||
// There's a race condition between when the stream is aborted and when the promise
|
||||
// resolves so we track whether we already aborted it to avoid writing twice.
|
||||
let aborted = false;
|
||||
function progress(
|
||||
entry:
|
||||
| {done: false, +value: ReactClientValue, ...}
|
||||
| {done: true, +value: ReactClientValue, ...},
|
||||
) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.done) {
|
||||
streamTask.status = COMPLETED;
|
||||
let endStreamRow;
|
||||
if (entry.value === undefined) {
|
||||
endStreamRow = streamTask.id.toString(16) + ':C\n';
|
||||
@@ -1067,13 +1075,10 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
request.completedRegularChunks.push(stringToChunk(endStreamRow));
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.cacheController.signal.removeEventListener(
|
||||
'abort',
|
||||
abortIterable,
|
||||
);
|
||||
enqueueFlush(request);
|
||||
request.abortListeners.delete(abortIterable);
|
||||
callOnAllReadyIfReady(request);
|
||||
aborted = true;
|
||||
} else {
|
||||
try {
|
||||
streamTask.model = entry.value;
|
||||
@@ -1092,10 +1097,11 @@ function serializeAsyncIterable(
|
||||
}
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (streamTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
request.cacheController.signal.removeEventListener('abort', abortIterable);
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortIterable);
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
@@ -1104,19 +1110,16 @@ function serializeAsyncIterable(
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
function abortIterable() {
|
||||
if (streamTask.status !== PENDING) {
|
||||
function abortIterable(reason: mixed) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortIterable);
|
||||
const reason = signal.reason;
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortIterable);
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(streamTask, request);
|
||||
request.abortableTasks.delete(streamTask);
|
||||
request.pendingChunks--;
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, streamTask, signal.reason);
|
||||
erroredTask(request, streamTask, reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
if (typeof (iterator: any).throw === 'function') {
|
||||
@@ -1125,7 +1128,7 @@ function serializeAsyncIterable(
|
||||
iterator.throw(reason).then(error, error);
|
||||
}
|
||||
}
|
||||
request.cacheController.signal.addEventListener('abort', abortIterable);
|
||||
request.abortListeners.add(abortIterable);
|
||||
if (__DEV__) {
|
||||
callIteratorInDEV(iterator, progress, error);
|
||||
} else {
|
||||
@@ -2149,11 +2152,7 @@ function visitAsyncNode(
|
||||
owner: node.owner,
|
||||
stack: filterStackTrace(request, node.stack),
|
||||
});
|
||||
// Mark the end time of the await. If we're aborting then we don't emit this
|
||||
// to signal that this never resolved inside this render.
|
||||
if (request.status !== ABORTING) {
|
||||
markOperationEndTime(request, task, endTime);
|
||||
}
|
||||
markOperationEndTime(request, task, endTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2214,12 +2213,7 @@ function emitAsyncSequence(
|
||||
}
|
||||
}
|
||||
emitDebugChunk(request, task.id, debugInfo);
|
||||
// Mark the end time of the await. If we're aborting then we don't emit this
|
||||
// to signal that this never resolved inside this render.
|
||||
if (request.status !== ABORTING) {
|
||||
// If we're currently aborting, then this never resolved into user space.
|
||||
markOperationEndTime(request, task, awaitedNode.end);
|
||||
}
|
||||
markOperationEndTime(request, task, awaitedNode.end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2681,14 +2675,16 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
|
||||
const reader = blob.stream().getReader();
|
||||
|
||||
let aborted = false;
|
||||
function progress(
|
||||
entry: {done: false, value: Uint8Array} | {done: true, value: void},
|
||||
): Promise<void> | void {
|
||||
if (newTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
if (entry.done) {
|
||||
request.cacheController.signal.removeEventListener('abort', abortBlob);
|
||||
request.abortListeners.delete(abortBlob);
|
||||
aborted = true;
|
||||
pingTask(request, newTask);
|
||||
return;
|
||||
}
|
||||
@@ -2698,26 +2694,25 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
return reader.read().then(progress).catch(error);
|
||||
}
|
||||
function error(reason: mixed) {
|
||||
if (newTask.status !== PENDING) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
request.cacheController.signal.removeEventListener('abort', abortBlob);
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortBlob);
|
||||
erroredTask(request, newTask, reason);
|
||||
enqueueFlush(request);
|
||||
// $FlowFixMe should be able to pass mixed
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
function abortBlob() {
|
||||
if (newTask.status !== PENDING) {
|
||||
function abortBlob(reason: mixed) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
const signal = request.cacheController.signal;
|
||||
signal.removeEventListener('abort', abortBlob);
|
||||
const reason = signal.reason;
|
||||
aborted = true;
|
||||
request.abortListeners.delete(abortBlob);
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
haltTask(newTask, request);
|
||||
request.pendingChunks--;
|
||||
} else {
|
||||
// TODO: Make this use abortTask() instead.
|
||||
erroredTask(request, newTask, reason);
|
||||
enqueueFlush(request);
|
||||
}
|
||||
@@ -2725,7 +2720,7 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
reader.cancel(reason).then(error, error);
|
||||
}
|
||||
|
||||
request.cacheController.signal.addEventListener('abort', abortBlob);
|
||||
request.abortListeners.add(abortBlob);
|
||||
|
||||
// $FlowFixMe[incompatible-call]
|
||||
reader.read().then(progress).catch(error);
|
||||
@@ -3919,13 +3914,6 @@ function serializeIONode(
|
||||
// The environment name may have changed from when the I/O was actually started.
|
||||
const env = (0, request.environmentName)();
|
||||
|
||||
const endTime =
|
||||
ioNode.tag === UNRESOLVED_PROMISE_NODE
|
||||
? // Mark the end time as now. It's arbitrary since it's not resolved but this
|
||||
// marks when we stopped trying.
|
||||
performance.now()
|
||||
: ioNode.end;
|
||||
|
||||
request.pendingChunks++;
|
||||
const id = request.nextChunkId++;
|
||||
emitIOInfoChunk(
|
||||
@@ -3933,7 +3921,7 @@ function serializeIONode(
|
||||
id,
|
||||
name,
|
||||
ioNode.start,
|
||||
endTime,
|
||||
ioNode.end,
|
||||
value,
|
||||
env,
|
||||
owner,
|
||||
@@ -4757,6 +4745,7 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
|
||||
env: env,
|
||||
};
|
||||
emitDebugChunk(request, task.id, asyncInfo);
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
} else {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
|
||||
}
|
||||
@@ -5016,15 +5005,16 @@ function retryTask(request: Request, task: Task): void {
|
||||
} catch (thrownValue) {
|
||||
if (request.status === ABORTING) {
|
||||
request.abortableTasks.delete(task);
|
||||
task.status = PENDING;
|
||||
task.status = ABORTED;
|
||||
if (enableHalt && request.type === PRERENDER) {
|
||||
// When aborting a prerener with halt semantics we don't emit
|
||||
// anything into the slot for a task that aborts, it remains unresolved
|
||||
haltTask(task, request);
|
||||
request.pendingChunks--;
|
||||
} else {
|
||||
// Otherwise we emit an error chunk into the task slot.
|
||||
const errorId: number = (request.fatalError: any);
|
||||
abortTask(task, request, errorId);
|
||||
const model = stringify(serializeByValueID(errorId));
|
||||
emitModelChunk(request, task.id, model);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -5267,9 +5257,8 @@ function enqueueFlush(request: Request): void {
|
||||
}
|
||||
|
||||
function callOnAllReadyIfReady(request: Request): void {
|
||||
if (request.abortableTasks.size === 0) {
|
||||
const onAllReady = request.onAllReady;
|
||||
onAllReady();
|
||||
if (request.abortableTasks.size === 0 && request.abortListeners.size === 0) {
|
||||
request.onAllReady();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5305,7 +5294,6 @@ export function abort(request: Request, reason: mixed): void {
|
||||
if (request.status <= OPEN) {
|
||||
request.status = ABORTING;
|
||||
request.cacheController.abort(reason);
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
const abortableTasks = request.abortableTasks;
|
||||
if (abortableTasks.size > 0) {
|
||||
@@ -5357,6 +5345,37 @@ export function abort(request: Request, reason: mixed): void {
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
}
|
||||
const abortListeners = request.abortListeners;
|
||||
if (abortListeners.size > 0) {
|
||||
let error;
|
||||
if (
|
||||
enablePostpone &&
|
||||
typeof reason === 'object' &&
|
||||
reason !== null &&
|
||||
(reason: any).$$typeof === REACT_POSTPONE_TYPE
|
||||
) {
|
||||
// We aborted with a Postpone but since we're passing this to an
|
||||
// external handler, passing this object would leak it outside React.
|
||||
// We create an alternative reason for it instead.
|
||||
error = new Error('The render was aborted due to being postponed.');
|
||||
} else {
|
||||
error =
|
||||
reason === undefined
|
||||
? new Error(
|
||||
'The render was aborted by the server without a reason.',
|
||||
)
|
||||
: typeof reason === 'object' &&
|
||||
reason !== null &&
|
||||
typeof reason.then === 'function'
|
||||
? new Error(
|
||||
'The render was aborted by the server with a promise.',
|
||||
)
|
||||
: reason;
|
||||
}
|
||||
abortListeners.forEach(callback => callback(error));
|
||||
abortListeners.clear();
|
||||
callOnAllReadyIfReady(request);
|
||||
}
|
||||
if (request.destination !== null) {
|
||||
flushCompletedChunks(request, request.destination);
|
||||
}
|
||||
|
||||
10
scripts/flow/react-native-host-hooks.js
vendored
10
scripts/flow/react-native-host-hooks.js
vendored
@@ -32,7 +32,6 @@ type __MeasureLayoutOnSuccessCallback = (
|
||||
type __ReactNativeBaseComponentViewConfig = any;
|
||||
type __ViewConfigGetter = any;
|
||||
type __ViewConfig = any;
|
||||
type __AttributeConfiguration = any;
|
||||
|
||||
// libdefs cannot actually import. This is supposed to be the type imported
|
||||
// from 'react-native-renderer/src/legacy-events/TopLevelEventTypes';
|
||||
@@ -204,15 +203,6 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
|
||||
declare export function getInternalInstanceHandleFromPublicInstance(
|
||||
publicInstance: PublicInstance,
|
||||
): ?Object;
|
||||
declare export function createAttributePayload(
|
||||
props: Object,
|
||||
validAttributes: __AttributeConfiguration,
|
||||
): null | Object;
|
||||
declare export function diffAttributePayloads(
|
||||
prevProps: Object,
|
||||
nextProps: Object,
|
||||
validAttributes: __AttributeConfiguration,
|
||||
): null | Object;
|
||||
}
|
||||
|
||||
declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {
|
||||
|
||||
Reference in New Issue
Block a user