mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Update base for Update on "[compiler] Migrate PruneNonEscapingScopes to HIR"
Summary: PruneNonEscapingScopes does a pretty powerful escape analysis, which we might want to apply for other purposes in our HIR passes. This ports this pass to HIR. For the most part, this implementation is identical to the ReactiveFunction version. It now handles phis instead of conditional ReactiveExpressions, which it does by treating all the phi operands as possibly aliasing the lvalue. This also requires that we iterate the aliasing analysis to a fixpoint, because the HIR has backedges which the ReactiveFunctions don't. In our fixtures, this only changes one result, which appears to have become more accurate. I plan on testing this internally in a sync before landing. [ghstack-poisoned]
This commit is contained in:
+5
-1
@@ -303,7 +303,6 @@ module.exports = {
|
||||
ERROR,
|
||||
{isProductionUserAppCode: true},
|
||||
],
|
||||
'react-internal/no-to-warn-dev-within-to-throw': ERROR,
|
||||
'react-internal/warning-args': ERROR,
|
||||
'react-internal/no-production-logging': ERROR,
|
||||
},
|
||||
@@ -590,6 +589,11 @@ module.exports = {
|
||||
WheelEventHandler: 'readonly',
|
||||
FinalizationRegistry: 'readonly',
|
||||
Omit: 'readonly',
|
||||
Keyframe: 'readonly',
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
KeyframeAnimationOptions: 'readonly',
|
||||
GetAnimationsOptions: 'readonly',
|
||||
Animatable: 'readonly',
|
||||
|
||||
spyOnDev: 'readonly',
|
||||
spyOnDevAndProd: 'readonly',
|
||||
|
||||
@@ -38,11 +38,7 @@ jobs:
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
|
||||
- name: yarn install compiler
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: compiler
|
||||
- name: yarn install playground
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: CI=true yarn test
|
||||
- run: ls -R test-results
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: (Shared) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: ${{ github.event.label.name == 'React Core Team' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v6.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.pull_request.user.login }}
|
||||
embed-author-url: ${{ github.event.pull_request.user.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
|
||||
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
|
||||
embed-description: ${{ github.event.pull_request.body }}
|
||||
embed-url: ${{ github.event.pull_request.html_url }}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode(all)
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = {};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -79,6 +79,24 @@ function Foo() {
|
||||
// @flow
|
||||
function useFoo(propVal: {+baz: number}) {
|
||||
return <div>{(propVal.baz as number)}</div>;
|
||||
}
|
||||
`,
|
||||
noFormat: true,
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-infer',
|
||||
input: `// @compilationMode(infer)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
`,
|
||||
noFormat: true,
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-all',
|
||||
input: `// @compilationMode(all)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
`,
|
||||
noFormat: true,
|
||||
|
||||
@@ -20,7 +20,6 @@ import BabelPluginReactCompiler, {
|
||||
CompilerPipelineValue,
|
||||
parsePluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {type EnvironmentConfig} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
import clsx from 'clsx';
|
||||
import invariant from 'invariant';
|
||||
import {useSnackbar} from 'notistack';
|
||||
@@ -69,24 +68,14 @@ function parseInput(
|
||||
function invokeCompiler(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
environment: EnvironmentConfig,
|
||||
logIR: (pipelineValue: CompilerPipelineValue) => void,
|
||||
options: PluginOptions,
|
||||
): CompilerTransformOutput {
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
},
|
||||
environment,
|
||||
compilationMode: 'all',
|
||||
panicThreshold: 'all_errors',
|
||||
});
|
||||
const ast = parseInput(source, language);
|
||||
let result = transformFromAstSync(ast, source, {
|
||||
filename: '_playgroundFile.js',
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [[BabelPluginReactCompiler, opts]],
|
||||
plugins: [[BabelPluginReactCompiler, options]],
|
||||
ast: true,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
@@ -172,51 +161,59 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const config = parseConfigPragmaForTests(pragma);
|
||||
|
||||
transformOutput = invokeCompiler(
|
||||
source,
|
||||
language,
|
||||
{...config, customHooks: new Map([...COMMON_HOOKS])},
|
||||
result => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
upsert({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
upsert({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
upsert({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const parsedOptions = parseConfigPragmaForTests(pragma, {
|
||||
compilationMode: 'infer',
|
||||
});
|
||||
const opts: PluginOptions = parsePluginOptions({
|
||||
...parsedOptions,
|
||||
environment: {
|
||||
...parsedOptions.environment,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
);
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
},
|
||||
});
|
||||
transformOutput = invokeCompiler(source, language, opts);
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
|
||||
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run watch\" \"yarn workspace react-compiler-runtime run watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
|
||||
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
|
||||
"build": "yarn build:compiler && next build",
|
||||
"postbuild": "node ./scripts/downloadFonts.js",
|
||||
"preinstall": "cd ../.. && yarn install --frozen-lockfile",
|
||||
"postinstall": "./scripts/link-compiler.sh",
|
||||
"vercel-build": "yarn build",
|
||||
"start": "next start",
|
||||
|
||||
+8
-10
@@ -15,7 +15,7 @@
|
||||
"start": "yarn workspace playground run start",
|
||||
"next": "yarn workspace playground run dev",
|
||||
"build": "yarn workspaces run build",
|
||||
"dev": "echo 'DEPRECATED: use `cd apps/playground && yarn dev` instead!' && sleep 5 && cd apps/playground && yarn dev",
|
||||
"dev": "cd apps/playground && yarn dev",
|
||||
"test": "yarn workspaces run test",
|
||||
"snap": "yarn workspace babel-plugin-react-compiler run snap",
|
||||
"snap:build": "yarn workspace snap run build",
|
||||
@@ -26,25 +26,23 @@
|
||||
"react-is": "0.0.0-experimental-4beb1fd8-20241118"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"concurrently": "^7.4.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"folder-hash": "^4.0.4",
|
||||
"npm-dts": "^1.3.13",
|
||||
"object-assign": "^4.1.1",
|
||||
"ora": "5.4.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-hermes-parser": "^0.25.1",
|
||||
"prettier-plugin-hermes-parser": "^0.26.0",
|
||||
"prompt-promise": "^1.0.3",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-banner2": "^1.2.3",
|
||||
"rollup-plugin-prettier": "^4.1.1",
|
||||
"rimraf": "^5.0.10",
|
||||
"typescript": "^5.4.3",
|
||||
"wait-on": "^7.2.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"rimraf": "5.0.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
"!*.tsbuildinfo"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
|
||||
"build": "rimraf dist && scripts/build.js",
|
||||
"test": "./scripts/link-react-compiler-runtime.sh && yarn snap:ci",
|
||||
"jest": "yarn build && ts-node node_modules/.bin/jest",
|
||||
"snap": "node ../snap/dist/main.js",
|
||||
"snap:build": "yarn workspace snap run build",
|
||||
"snap:ci": "yarn snap:build && yarn snap",
|
||||
"ts:analyze-trace": "scripts/ts-analyze-trace.sh",
|
||||
"lint": "yarn eslint src"
|
||||
"lint": "yarn eslint src",
|
||||
"watch": "scripts/build.js --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.19.0"
|
||||
@@ -49,7 +50,6 @@
|
||||
"pretty-format": "^24",
|
||||
"react": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.22.4",
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import prettier from 'rollup-plugin-prettier';
|
||||
import banner2 from 'rollup-plugin-banner2';
|
||||
|
||||
const NO_INLINE = new Set(['@babel/types']);
|
||||
|
||||
const DEV_ROLLUP_CONFIG = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
exports: 'named',
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
outputToFilesystem: true,
|
||||
compilerOptions: {
|
||||
noEmit: true,
|
||||
},
|
||||
}),
|
||||
json(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
resolveOnly: module => NO_INLINE.has(module) === false,
|
||||
rootDir: path.join(process.cwd(), '..'),
|
||||
}),
|
||||
commonjs(),
|
||||
terser({
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: false,
|
||||
mangle: false,
|
||||
}),
|
||||
prettier(),
|
||||
banner2(
|
||||
() => `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";
|
||||
`
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default DEV_ROLLUP_CONFIG;
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
|
||||
const argv = yargs(process.argv.slice(2))
|
||||
.options('w', {
|
||||
alias: 'watch',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.parse();
|
||||
|
||||
const config = {
|
||||
entryPoints: [path.join(__dirname, '../src/index.ts')],
|
||||
outfile: path.join(__dirname, '../dist/index.js'),
|
||||
bundle: true,
|
||||
external: ['@babel/types'],
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
banner: {
|
||||
js: `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";`,
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (argv.w) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('watching for changes...');
|
||||
} else {
|
||||
await esbuild.build({
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -9,7 +9,13 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Logger} from '../Entrypoint';
|
||||
import {
|
||||
CompilationMode,
|
||||
Logger,
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -683,7 +689,9 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: any = {};
|
||||
// Get the defaults to programmatically check for boolean properties
|
||||
const defaultConfig = EnvironmentConfigSchema.parse({});
|
||||
@@ -749,6 +757,48 @@ export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
let compilationMode: CompilationMode = defaults.compilationMode;
|
||||
let panicThreshold: PanicThresholdOptions = 'all_errors';
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
switch (token) {
|
||||
case '@compilationMode(annotation)': {
|
||||
compilationMode = 'annotation';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(infer)': {
|
||||
compilationMode = 'infer';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(all)': {
|
||||
compilationMode = 'all';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(syntax)': {
|
||||
compilationMode = 'syntax';
|
||||
break;
|
||||
}
|
||||
case '@panicThreshold(none)': {
|
||||
panicThreshold = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions({
|
||||
environment,
|
||||
compilationMode,
|
||||
panicThreshold,
|
||||
});
|
||||
}
|
||||
|
||||
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
|
||||
|
||||
|
||||
+13
-10
@@ -12,9 +12,12 @@ function Component(props) {
|
||||
|
||||
const deps = [foo, props];
|
||||
|
||||
useEffect(() => {
|
||||
fire(foo(props));
|
||||
}, ...deps);
|
||||
useEffect(
|
||||
() => {
|
||||
fire(foo(props));
|
||||
},
|
||||
...deps
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -25,13 +28,13 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
11 | useEffect(() => {
|
||||
12 | fire(foo(props));
|
||||
> 13 | }, ...deps);
|
||||
| ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13)
|
||||
14 |
|
||||
15 | return null;
|
||||
16 | }
|
||||
13 | fire(foo(props));
|
||||
14 | },
|
||||
> 15 | ...deps
|
||||
| ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (15:15)
|
||||
16 | );
|
||||
17 |
|
||||
18 | return null;
|
||||
```
|
||||
|
||||
|
||||
+11
-5
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {parseConfigPragmaForTests, validateEnvironmentConfig} from '..';
|
||||
import {defaultOptions} from '../Entrypoint';
|
||||
|
||||
describe('parseConfigPragmaForTests()', () => {
|
||||
it('parses flags in various forms', () => {
|
||||
@@ -19,13 +20,18 @@ describe('parseConfigPragmaForTests()', () => {
|
||||
|
||||
const config = parseConfigPragmaForTests(
|
||||
'@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false',
|
||||
{compilationMode: defaultOptions.compilationMode},
|
||||
);
|
||||
expect(config).toEqual({
|
||||
...defaultConfig,
|
||||
enableUseTypeAnnotations: true,
|
||||
validateNoSetStateInPassiveEffects: true,
|
||||
validateNoSetStateInRender: false,
|
||||
enableResetCacheOnSourceFileChanges: false,
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
environment: {
|
||||
...defaultOptions.environment,
|
||||
enableUseTypeAnnotations: true,
|
||||
validateNoSetStateInPassiveEffects: true,
|
||||
validateNoSetStateInRender: false,
|
||||
enableResetCacheOnSourceFileChanges: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"description": "ESLint plugin to display errors found by the React compiler.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
|
||||
"test": "tsc && jest"
|
||||
"build": "rimraf dist && scripts/build.js",
|
||||
"test": "tsc && jest",
|
||||
"watch": "scripts/build.js --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import prettier from 'rollup-plugin-prettier';
|
||||
import banner2 from 'rollup-plugin-banner2';
|
||||
|
||||
const NO_INLINE = new Set([
|
||||
'@babel/core',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
'hermes-parser',
|
||||
'zod',
|
||||
'zod-validation-error',
|
||||
]);
|
||||
|
||||
const DEV_ROLLUP_CONFIG = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
},
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
compilerOptions: {
|
||||
noEmit: true,
|
||||
},
|
||||
}),
|
||||
json(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
resolveOnly: module => NO_INLINE.has(module) === false,
|
||||
rootDir: path.join(process.cwd(), '..'),
|
||||
}),
|
||||
commonjs(),
|
||||
terser({
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: false,
|
||||
mangle: false,
|
||||
}),
|
||||
prettier(),
|
||||
banner2(
|
||||
() => `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";
|
||||
`
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default DEV_ROLLUP_CONFIG;
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
|
||||
const argv = yargs(process.argv.slice(2))
|
||||
.options('w', {
|
||||
alias: 'watch',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.parse();
|
||||
|
||||
const config = {
|
||||
entryPoints: [path.join(__dirname, '../src/index.ts')],
|
||||
outfile: path.join(__dirname, '../dist/index.js'),
|
||||
bundle: true,
|
||||
external: [
|
||||
'@babel/core',
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
'hermes-parser',
|
||||
'zod',
|
||||
'zod-validation-error',
|
||||
],
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
banner: {
|
||||
js: `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";`,
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (argv.w) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('watching for changes...');
|
||||
} else {
|
||||
await esbuild.build({
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "make-read-only-util",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest src"
|
||||
"build": "rimraf dist && scripts/build.js",
|
||||
"test": "jest src",
|
||||
"watch": "scripts/build.js --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
|
||||
const argv = yargs(process.argv.slice(2))
|
||||
.options('w', {
|
||||
alias: 'watch',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.parse();
|
||||
|
||||
const config = {
|
||||
entryPoints: [path.join(__dirname, '../src/makeReadOnly.ts')],
|
||||
outfile: path.join(__dirname, '../dist/index.js'),
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'browser',
|
||||
target: 'es6',
|
||||
banner: {
|
||||
js: `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";`,
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (argv.w) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('watching for changes...');
|
||||
} else {
|
||||
await esbuild.build({
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -6,8 +6,9 @@
|
||||
"react-compiler-healthcheck": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
|
||||
"test": "echo 'no tests'"
|
||||
"build": "rimraf dist && scripts/build.js",
|
||||
"test": "echo 'no tests'",
|
||||
"watch": "scripts/build.js --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import prettier from 'rollup-plugin-prettier';
|
||||
import banner2 from 'rollup-plugin-banner2';
|
||||
|
||||
const NO_INLINE = new Set([
|
||||
'@babel/core',
|
||||
'@babel/parser',
|
||||
'chalk',
|
||||
'fast-glob',
|
||||
'ora',
|
||||
'yargs',
|
||||
'zod',
|
||||
'zod-validation-error',
|
||||
]);
|
||||
|
||||
const DEV_ROLLUP_CONFIG = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
exports: 'named',
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
compilerOptions: {
|
||||
noEmit: true,
|
||||
},
|
||||
}),
|
||||
json(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
resolveOnly: module => NO_INLINE.has(module) === false,
|
||||
rootDir: path.join(process.cwd(), '..'),
|
||||
}),
|
||||
commonjs(),
|
||||
terser({
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
compress: false,
|
||||
mangle: false,
|
||||
}),
|
||||
prettier(),
|
||||
banner2(
|
||||
() => `#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";
|
||||
`
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default DEV_ROLLUP_CONFIG;
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
|
||||
const argv = yargs(process.argv.slice(2))
|
||||
.options('w', {
|
||||
alias: 'watch',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.parse();
|
||||
|
||||
const config = {
|
||||
entryPoints: [path.join(__dirname, '../src/index.ts')],
|
||||
outfile: path.join(__dirname, '../dist/index.js'),
|
||||
bundle: true,
|
||||
external: [
|
||||
'@babel/core',
|
||||
'@babel/parser',
|
||||
'chalk',
|
||||
'fast-glob',
|
||||
'ora',
|
||||
'yargs',
|
||||
'zod',
|
||||
'zod-validation-error',
|
||||
],
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
banner: {
|
||||
js: `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";`,
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (argv.w) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('watching for changes...');
|
||||
} else {
|
||||
await esbuild.build({
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -4,16 +4,18 @@
|
||||
"description": "Runtime for React Compiler",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
|
||||
"test": "echo 'no tests'"
|
||||
"build": "rimraf dist && scripts/build.js",
|
||||
"test": "echo 'no tests'",
|
||||
"watch": "scripts/build.js --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import banner2 from 'rollup-plugin-banner2';
|
||||
|
||||
const NO_INLINE = new Set(['react']);
|
||||
|
||||
const PROD_ROLLUP_CONFIG = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
compilerOptions: {
|
||||
noEmit: true,
|
||||
},
|
||||
}),
|
||||
json(),
|
||||
nodeResolve({
|
||||
preferBuiltins: true,
|
||||
resolveOnly: module => NO_INLINE.has(module) === false,
|
||||
rootDir: path.join(process.cwd(), '..'),
|
||||
}),
|
||||
commonjs(),
|
||||
banner2(
|
||||
() => `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";` // DO NOT REMOVE
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default PROD_ROLLUP_CONFIG;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
const {Generator} = require('npm-dts');
|
||||
|
||||
const argv = yargs(process.argv.slice(2))
|
||||
.options('p', {
|
||||
alias: 'platform',
|
||||
default: 'browser',
|
||||
choices: ['browser', 'node'],
|
||||
})
|
||||
.options('w', {
|
||||
alias: 'watch',
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
.parse();
|
||||
|
||||
const config = {
|
||||
entryPoints: [path.join(__dirname, '../src/index.ts')],
|
||||
outfile: path.join(__dirname, '../dist/index.js'),
|
||||
bundle: true,
|
||||
external: ['react'],
|
||||
format: 'cjs',
|
||||
platform: argv.p,
|
||||
target: 'es6',
|
||||
banner: {
|
||||
js: `/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/
|
||||
|
||||
"use no memo";`,
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (argv.w) {
|
||||
const ctx = await esbuild.context(config);
|
||||
await ctx.watch();
|
||||
console.log('watching for changes...');
|
||||
} else {
|
||||
await esbuild.build({
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
...config,
|
||||
});
|
||||
await new Generator({
|
||||
entry: 'src/index.ts',
|
||||
output: 'dist/index.d.ts',
|
||||
}).generate();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -5,8 +5,6 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
'use no memo';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
const {useRef, useEffect, isValidElement} = React;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "./scripts/link-react-compiler-runtime.sh && perl -p -i -e 's/react\\.element/react.transitional.element/' ../../node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' ../../node_modules/react-dom/cjs/react-dom-test-utils.development.js",
|
||||
"build": "rimraf dist && concurrently -n snap,runtime \"tsc --build\" \"yarn --silent workspace react-compiler-runtime build --silent\"",
|
||||
"build": "rimraf dist && concurrently -n snap,runtime \"tsc --build\" \"yarn --silent workspace react-compiler-runtime build -p node\"",
|
||||
"test": "echo 'no tests'",
|
||||
"prettier": "prettier --write 'src/**/*.ts'"
|
||||
},
|
||||
@@ -51,8 +51,7 @@
|
||||
"@types/node": "^18.7.18",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"rimraf": "^3.0.2"
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
|
||||
@@ -11,12 +11,9 @@ import {transformFromAstSync} from '@babel/core';
|
||||
import * as BabelParser from '@babel/parser';
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import assert from 'assert';
|
||||
import type {
|
||||
CompilationMode,
|
||||
Logger,
|
||||
LoggerEvent,
|
||||
PanicThresholdOptions,
|
||||
PluginOptions,
|
||||
CompilerReactTarget,
|
||||
CompilerPipelineValue,
|
||||
@@ -51,31 +48,13 @@ function makePluginOptions(
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
|
||||
let gating = null;
|
||||
let compilationMode: CompilationMode = 'all';
|
||||
let panicThreshold: PanicThresholdOptions = 'all_errors';
|
||||
let hookPattern: string | null = null;
|
||||
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
||||
let validatePreserveExistingMemoizationGuarantees = false;
|
||||
let customMacros: null | Array<Macro> = null;
|
||||
let validateBlocklistedImports = null;
|
||||
let enableFire = false;
|
||||
let target: CompilerReactTarget = '19';
|
||||
|
||||
if (firstLine.indexOf('@compilationMode(annotation)') !== -1) {
|
||||
assert(
|
||||
compilationMode === 'all',
|
||||
'Cannot set @compilationMode(..) more than once',
|
||||
);
|
||||
compilationMode = 'annotation';
|
||||
}
|
||||
if (firstLine.indexOf('@compilationMode(infer)') !== -1) {
|
||||
assert(
|
||||
compilationMode === 'all',
|
||||
'Cannot set @compilationMode(..) more than once',
|
||||
);
|
||||
compilationMode = 'infer';
|
||||
}
|
||||
|
||||
if (firstLine.includes('@gating')) {
|
||||
gating = {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
@@ -96,10 +75,6 @@ function makePluginOptions(
|
||||
}
|
||||
}
|
||||
|
||||
if (firstLine.includes('@panicThreshold(none)')) {
|
||||
panicThreshold = 'none';
|
||||
}
|
||||
|
||||
let eslintSuppressionRules: Array<string> | null = null;
|
||||
const eslintSuppressionMatch = /@eslintSuppressionRules\(([^)]+)\)/.exec(
|
||||
firstLine,
|
||||
@@ -130,10 +105,6 @@ function makePluginOptions(
|
||||
validatePreserveExistingMemoizationGuarantees = true;
|
||||
}
|
||||
|
||||
if (firstLine.includes('@enableFire')) {
|
||||
enableFire = true;
|
||||
}
|
||||
|
||||
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
|
||||
if (
|
||||
hookPatternMatch &&
|
||||
@@ -199,10 +170,11 @@ function makePluginOptions(
|
||||
debugLogIRs: debugIRLogger,
|
||||
};
|
||||
|
||||
const config = parseConfigPragmaFn(firstLine);
|
||||
const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});
|
||||
const options = {
|
||||
...config,
|
||||
environment: {
|
||||
...config,
|
||||
...config.environment,
|
||||
moduleTypeProvider: makeSharedRuntimeTypeProvider({
|
||||
EffectEnum,
|
||||
ValueKindEnum,
|
||||
@@ -212,12 +184,9 @@ function makePluginOptions(
|
||||
hookPattern,
|
||||
validatePreserveExistingMemoizationGuarantees,
|
||||
validateBlocklistedImports,
|
||||
enableFire,
|
||||
},
|
||||
compilationMode,
|
||||
logger,
|
||||
gating,
|
||||
panicThreshold,
|
||||
noEmit: false,
|
||||
eslintSuppressionRules,
|
||||
flowSuppressions,
|
||||
|
||||
@@ -4,7 +4,7 @@ const {execHelper} = require('./utils');
|
||||
async function buildPackages(pkgNames) {
|
||||
const spinner = ora(`Building packages`).info();
|
||||
for (const pkgName of pkgNames) {
|
||||
const command = `yarn workspace ${pkgName} run build`;
|
||||
const command = `NODE_ENV=production yarn workspace ${pkgName} run build`;
|
||||
spinner.start(`Running: ${command}\n`);
|
||||
try {
|
||||
await execHelper(command);
|
||||
|
||||
+627
-423
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,6 @@ let TestAct;
|
||||
|
||||
global.__DEV__ = process.env.NODE_ENV !== 'production';
|
||||
|
||||
expect.extend(require('../toWarnDev'));
|
||||
|
||||
describe('unmocked scheduler', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
// copied from scripts/jest/matchers/toWarnDev.js
|
||||
'use strict';
|
||||
|
||||
const {diff: jestDiff} = require('jest-diff');
|
||||
const util = require('util');
|
||||
|
||||
function shouldIgnoreConsoleError(format, args) {
|
||||
if (__DEV__) {
|
||||
if (typeof format === 'string') {
|
||||
if (format.indexOf('The above error occurred') === 0) {
|
||||
// This looks like an error addendum from ReactFiberErrorLogger.
|
||||
// Ignore it too.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
format != null &&
|
||||
typeof format.message === 'string' &&
|
||||
typeof format.stack === 'string' &&
|
||||
args.length === 0
|
||||
) {
|
||||
// In production, ReactFiberErrorLogger logs error objects directly.
|
||||
// They are noisy too so we'll try to ignore them.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Looks legit
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeCodeLocInfo(str) {
|
||||
return str && str.replace(/at .+?:\d+/g, 'at **');
|
||||
}
|
||||
|
||||
const createMatcherFor = consoleMethod =>
|
||||
function matcher(callback, expectedMessages, options = {}) {
|
||||
if (__DEV__) {
|
||||
// Warn about incorrect usage of matcher.
|
||||
if (typeof expectedMessages === 'string') {
|
||||
expectedMessages = [expectedMessages];
|
||||
} else if (!Array.isArray(expectedMessages)) {
|
||||
throw Error(
|
||||
`toWarnDev() requires a parameter of type string or an array of strings ` +
|
||||
`but was given ${typeof expectedMessages}.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
options != null &&
|
||||
(typeof options !== 'object' || Array.isArray(options))
|
||||
) {
|
||||
throw new Error(
|
||||
'toWarnDev() second argument, when present, should be an object. ' +
|
||||
'Did you forget to wrap the messages into an array?'
|
||||
);
|
||||
}
|
||||
if (arguments.length > 3) {
|
||||
// `matcher` comes from Jest, so it's more than 2 in practice
|
||||
throw new Error(
|
||||
'toWarnDev() received more than two arguments. ' +
|
||||
'Did you forget to wrap the messages into an array?'
|
||||
);
|
||||
}
|
||||
|
||||
const withoutStack = options.withoutStack;
|
||||
const warningsWithoutComponentStack = [];
|
||||
const warningsWithComponentStack = [];
|
||||
const unexpectedWarnings = [];
|
||||
|
||||
let lastWarningWithMismatchingFormat = null;
|
||||
let lastWarningWithExtraComponentStack = null;
|
||||
|
||||
// Catch errors thrown by the callback,
|
||||
// But only rethrow them if all test expectations have been satisfied.
|
||||
// Otherwise an Error in the callback can mask a failed expectation,
|
||||
// and result in a test that passes when it shouldn't.
|
||||
let caughtError;
|
||||
|
||||
const isLikelyAComponentStack = message =>
|
||||
typeof message === 'string' && message.includes('\n in ');
|
||||
|
||||
const consoleSpy = (format, ...args) => {
|
||||
// Ignore uncaught errors reported by jsdom
|
||||
// and React addendums because they're too noisy.
|
||||
if (
|
||||
consoleMethod === 'error' &&
|
||||
shouldIgnoreConsoleError(format, args)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = util.format(format, ...args);
|
||||
const normalizedMessage = normalizeCodeLocInfo(message);
|
||||
|
||||
// Remember if the number of %s interpolations
|
||||
// doesn't match the number of arguments.
|
||||
// We'll fail the test if it happens.
|
||||
let argIndex = 0;
|
||||
format.replace(/%s/g, () => argIndex++);
|
||||
if (argIndex !== args.length) {
|
||||
lastWarningWithMismatchingFormat = {
|
||||
format,
|
||||
args,
|
||||
expectedArgCount: argIndex,
|
||||
};
|
||||
}
|
||||
|
||||
// Protect against accidentally passing a component stack
|
||||
// to warning() which already injects the component stack.
|
||||
if (
|
||||
args.length >= 2 &&
|
||||
isLikelyAComponentStack(args[args.length - 1]) &&
|
||||
isLikelyAComponentStack(args[args.length - 2])
|
||||
) {
|
||||
lastWarningWithExtraComponentStack = {
|
||||
format,
|
||||
};
|
||||
}
|
||||
|
||||
for (let index = 0; index < expectedMessages.length; index++) {
|
||||
const expectedMessage = expectedMessages[index];
|
||||
if (
|
||||
normalizedMessage === expectedMessage ||
|
||||
normalizedMessage.includes(expectedMessage)
|
||||
) {
|
||||
if (isLikelyAComponentStack(normalizedMessage)) {
|
||||
warningsWithComponentStack.push(normalizedMessage);
|
||||
} else {
|
||||
warningsWithoutComponentStack.push(normalizedMessage);
|
||||
}
|
||||
expectedMessages.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let errorMessage;
|
||||
if (expectedMessages.length === 0) {
|
||||
errorMessage =
|
||||
'Unexpected warning recorded: ' +
|
||||
this.utils.printReceived(normalizedMessage);
|
||||
} else if (expectedMessages.length === 1) {
|
||||
errorMessage =
|
||||
'Unexpected warning recorded: ' +
|
||||
jestDiff(expectedMessages[0], normalizedMessage);
|
||||
} else {
|
||||
errorMessage =
|
||||
'Unexpected warning recorded: ' +
|
||||
jestDiff(expectedMessages, [normalizedMessage]);
|
||||
}
|
||||
|
||||
// Record the call stack for unexpected warnings.
|
||||
// We don't throw an Error here though,
|
||||
// Because it might be suppressed by ReactFiberScheduler.
|
||||
unexpectedWarnings.push(new Error(errorMessage));
|
||||
};
|
||||
|
||||
// TODO Decide whether we need to support nested toWarn* expectations.
|
||||
// If we don't need it, add a check here to see if this is already our spy,
|
||||
// And throw an error.
|
||||
const originalMethod = console[consoleMethod];
|
||||
|
||||
// Avoid using Jest's built-in spy since it can't be removed.
|
||||
console[consoleMethod] = consoleSpy;
|
||||
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
} finally {
|
||||
// Restore the unspied method so that unexpected errors fail tests.
|
||||
console[consoleMethod] = originalMethod;
|
||||
|
||||
// Any unexpected Errors thrown by the callback should fail the test.
|
||||
// This should take precedence since unexpected errors could block warnings.
|
||||
if (caughtError) {
|
||||
throw caughtError;
|
||||
}
|
||||
|
||||
// Any unexpected warnings should be treated as a failure.
|
||||
if (unexpectedWarnings.length > 0) {
|
||||
return {
|
||||
message: () => unexpectedWarnings[0].stack,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Any remaining messages indicate a failed expectations.
|
||||
if (expectedMessages.length > 0) {
|
||||
return {
|
||||
message: () =>
|
||||
`Expected warning was not recorded:\n ${this.utils.printReceived(
|
||||
expectedMessages[0]
|
||||
)}`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof withoutStack === 'number') {
|
||||
// We're expecting a particular number of warnings without stacks.
|
||||
if (withoutStack !== warningsWithoutComponentStack.length) {
|
||||
return {
|
||||
message: () =>
|
||||
`Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` +
|
||||
warningsWithoutComponentStack.map(warning =>
|
||||
this.utils.printReceived(warning)
|
||||
),
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
} else if (withoutStack === true) {
|
||||
// We're expecting that all warnings won't have the stack.
|
||||
// If some warnings have it, it's an error.
|
||||
if (warningsWithComponentStack.length > 0) {
|
||||
return {
|
||||
message: () =>
|
||||
`Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived(
|
||||
warningsWithComponentStack[0]
|
||||
)}\nIf this warning intentionally includes the component stack, remove ` +
|
||||
`{withoutStack: true} from the toWarnDev() call. If you have a mix of ` +
|
||||
`warnings with and without stack in one toWarnDev() call, pass ` +
|
||||
`{withoutStack: N} where N is the number of warnings without stacks.`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
} else if (withoutStack === false || withoutStack === undefined) {
|
||||
// We're expecting that all warnings *do* have the stack (default).
|
||||
// If some warnings don't have it, it's an error.
|
||||
if (warningsWithoutComponentStack.length > 0) {
|
||||
return {
|
||||
message: () =>
|
||||
`Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived(
|
||||
warningsWithoutComponentStack[0]
|
||||
)}\nIf this warning intentionally omits the component stack, add ` +
|
||||
`{withoutStack: true} to the toWarnDev() call.`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw Error(
|
||||
`The second argument for toWarnDev(), when specified, must be an object. It may have a ` +
|
||||
`property called "withoutStack" whose value may be undefined, boolean, or a number. ` +
|
||||
`Instead received ${typeof withoutStack}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (lastWarningWithMismatchingFormat !== null) {
|
||||
return {
|
||||
message: () =>
|
||||
`Received ${
|
||||
lastWarningWithMismatchingFormat.args.length
|
||||
} arguments for a message with ${
|
||||
lastWarningWithMismatchingFormat.expectedArgCount
|
||||
} placeholders:\n ${this.utils.printReceived(
|
||||
lastWarningWithMismatchingFormat.format
|
||||
)}`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastWarningWithExtraComponentStack !== null) {
|
||||
return {
|
||||
message: () =>
|
||||
`Received more than one component stack for a warning:\n ${this.utils.printReceived(
|
||||
lastWarningWithExtraComponentStack.format
|
||||
)}\nDid you accidentally pass a stack to warning() as the last argument? ` +
|
||||
`Don't forget warning() already injects the component stack automatically.`,
|
||||
pass: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {pass: true};
|
||||
}
|
||||
} else {
|
||||
// Any uncaught errors or warnings should fail tests in production mode.
|
||||
callback();
|
||||
|
||||
return {pass: true};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
toLowPriorityWarnDev: createMatcherFor('warn'),
|
||||
toWarnDev: createMatcherFor('error'),
|
||||
};
|
||||
@@ -18,7 +18,7 @@
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"",
|
||||
"dev": "concurrently \"npm run dev:watch\" \"sleep 2 && npm run dev:start\"",
|
||||
"dev:watch": "NODE_ENV=development parcel watch",
|
||||
"dev:start": "NODE_ENV=development node dist/server.js",
|
||||
"build": "parcel build",
|
||||
@@ -28,8 +28,8 @@
|
||||
"packageExports": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@parcel/config-default": "2.0.0-dev.1789",
|
||||
"@parcel/runtime-rsc": "2.13.3-dev.3412",
|
||||
"@parcel/config-default": "2.0.0-dev.1795",
|
||||
"@parcel/runtime-rsc": "2.13.3-dev.3418",
|
||||
"@types/parcel-env": "^0.0.6",
|
||||
"@types/express": "*",
|
||||
"@types/node": "^22.10.1",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^7.3.0",
|
||||
"express": "^4.18.2",
|
||||
"parcel": "2.0.0-dev.1787",
|
||||
"parcel": "2.0.0-dev.1793",
|
||||
"process": "^0.11.10",
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental",
|
||||
|
||||
@@ -15,8 +15,8 @@ import {injectRSCPayload} from 'rsc-html-stream/server';
|
||||
|
||||
// Client dependencies, used for SSR.
|
||||
// These must run in the same environment as client components (e.g. same instance of React).
|
||||
import {createFromReadableStream} from 'react-server-dom-parcel/client' with {env: 'react-client'};
|
||||
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server' with {env: 'react-client'};
|
||||
import {createFromReadableStream} from 'react-server-dom-parcel/client.edge' with {env: 'react-client'};
|
||||
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge' with {env: 'react-client'};
|
||||
import ReactClient, {ReactElement} from 'react' with {env: 'react-client'};
|
||||
|
||||
// Page components. These must have "use server-entry" so they are treated as code splitting entry points.
|
||||
@@ -66,8 +66,9 @@ async function render(
|
||||
|
||||
// Use client react to render the RSC payload to HTML.
|
||||
let [s1, s2] = stream.tee();
|
||||
let data = createFromReadableStream<ReactElement>(s1);
|
||||
let data: Promise<ReactElement>;
|
||||
function Content() {
|
||||
data ??= createFromReadableStream<ReactElement>(s1);
|
||||
return ReactClient.use(data);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+9
-1
@@ -2,13 +2,16 @@
|
||||
|
||||
declare module 'react-server-dom-parcel/client' {
|
||||
export function createFromFetch<T>(res: Promise<Response>): Promise<T>;
|
||||
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
|
||||
export function encodeReply(value: any): Promise<string | URLSearchParams | FormData>;
|
||||
|
||||
type CallServerCallback = <T>(id: string, args: any[]) => Promise<T>;
|
||||
export function setServerCallback(cb: CallServerCallback): void;
|
||||
}
|
||||
|
||||
declare module 'react-server-dom-parcel/client.edge' {
|
||||
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
|
||||
}
|
||||
|
||||
declare module 'react-server-dom-parcel/server.edge' {
|
||||
export function renderToReadableStream(value: any): ReadableStream;
|
||||
export function loadServerAction(id: string): Promise<(...args: any[]) => any>;
|
||||
@@ -17,5 +20,10 @@ declare module 'react-server-dom-parcel/server.edge' {
|
||||
}
|
||||
|
||||
declare module '@parcel/runtime-rsc' {
|
||||
import {JSX} from 'react';
|
||||
export function Resources(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module 'react-dom/server.edge' {
|
||||
export * from 'react-dom/server';
|
||||
}
|
||||
|
||||
+409
-408
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import {use, Suspense, useState, startTransition} from 'react';
|
||||
import {use, Suspense, useState, startTransition, Profiler} from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
|
||||
|
||||
@@ -54,14 +54,20 @@ async function hydrateApp() {
|
||||
}
|
||||
);
|
||||
|
||||
ReactDOM.hydrateRoot(document, <Shell data={root} />, {
|
||||
// TODO: This part doesn't actually work because the server only returns
|
||||
// form state during the request that submitted the form. Which means it
|
||||
// the state needs to be transported as part of the HTML stream. We intend
|
||||
// to add a feature to Fizz for this, but for now it's up to the
|
||||
// metaframework to implement correctly.
|
||||
formState: formState,
|
||||
});
|
||||
ReactDOM.hydrateRoot(
|
||||
document,
|
||||
<Profiler id="root">
|
||||
<Shell data={root} />
|
||||
</Profiler>,
|
||||
{
|
||||
// TODO: This part doesn't actually work because the server only returns
|
||||
// form state during the request that submitted the form. Which means it
|
||||
// the state needs to be transported as part of the HTML stream. We intend
|
||||
// to add a feature to Fizz for this, but for now it's up to the
|
||||
// metaframework to implement correctly.
|
||||
formState: formState,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Remove this line to simulate MPA behavior
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from 'react';
|
||||
import {Profiler} from 'react';
|
||||
import {hydrateRoot} from 'react-dom/client';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
hydrateRoot(document, <App assets={window.assetManifest} />);
|
||||
hydrateRoot(
|
||||
document,
|
||||
<Profiler id="root">
|
||||
<App assets={window.assetManifest} />
|
||||
</Profiler>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# View Transition
|
||||
|
||||
A test case for View Transitions.
|
||||
|
||||
## Setup
|
||||
|
||||
To reference a local build of React, first run `npm run build` at the root
|
||||
of the React project. Then:
|
||||
|
||||
```
|
||||
cd fixtures/view-transition
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.
|
||||
|
||||
**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:**
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
If you want to try the production mode instead run:
|
||||
|
||||
```
|
||||
yarn start:prod
|
||||
```
|
||||
|
||||
This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading).
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "react-fixtures-view-transition",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"concurrently": "3.1.0",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/register": "^7.25.9",
|
||||
"express": "^4.14.0",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prestart": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
||||
"dev:client": "PORT=3001 react-scripts start",
|
||||
"dev:server": "NODE_ENV=development node server",
|
||||
"start": "react-scripts build && NODE_ENV=production node server",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
/*
|
||||
This is just a placeholder to make react-scripts happy.
|
||||
We're not using it. If we end up here, redirect to the
|
||||
primary server.
|
||||
*/
|
||||
location.href = '//localhost:3000/';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
require('ignore-styles');
|
||||
const babelRegister = require('@babel/register');
|
||||
const proxy = require('http-proxy-middleware');
|
||||
|
||||
babelRegister({
|
||||
ignore: [/\/(build|node_modules)\//],
|
||||
presets: ['react-app'],
|
||||
});
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Application
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.get('/', function (req, res) {
|
||||
// In development mode we clear the module cache between each request to
|
||||
// get automatic hot reloading.
|
||||
for (var key in require.cache) {
|
||||
delete require.cache[key];
|
||||
}
|
||||
const render = require('./render').default;
|
||||
render(req.url, res);
|
||||
});
|
||||
} else {
|
||||
const render = require('./render').default;
|
||||
app.get('/', function (req, res) {
|
||||
render(req.url, res);
|
||||
});
|
||||
}
|
||||
|
||||
// Static resources
|
||||
app.use(express.static(path.resolve(__dirname, '..', 'build')));
|
||||
|
||||
// Proxy everything else to create-react-app's webpack development server
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(
|
||||
'/',
|
||||
proxy.createProxyMiddleware({
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
target: 'http://127.0.0.1:3001',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('Listening on port 3000...');
|
||||
});
|
||||
|
||||
app.on('error', function (error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
|
||||
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
|
||||
import App from '../src/components/App';
|
||||
|
||||
let assets;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Use the bundle from create-react-app's server in development mode.
|
||||
assets = {
|
||||
'main.js': '/static/js/bundle.js',
|
||||
// 'main.css': '',
|
||||
};
|
||||
} else {
|
||||
assets = require('../build/asset-manifest.json').files;
|
||||
}
|
||||
|
||||
export default function render(url, res) {
|
||||
res.socket.on('error', error => {
|
||||
// Log fatal errors
|
||||
console.error('Fatal', error);
|
||||
});
|
||||
let didError = false;
|
||||
const {pipe, abort} = renderToPipeableStream(
|
||||
<App assets={assets} initialURL={url} />,
|
||||
{
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
onShellReady() {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
res.statusCode = didError ? 500 : 200;
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
pipe(res);
|
||||
},
|
||||
onShellError(x) {
|
||||
// Something errored before we could complete the shell so we emit an alternative shell.
|
||||
res.statusCode = 500;
|
||||
res.send('<!doctype><p>Error</p>');
|
||||
},
|
||||
onError(x) {
|
||||
didError = true;
|
||||
console.error(x);
|
||||
},
|
||||
}
|
||||
);
|
||||
// Abandon and switch to client rendering after 5 seconds.
|
||||
// Try lowering this to see the client recover.
|
||||
setTimeout(abort, 5000);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, {
|
||||
startTransition,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import Chrome from './Chrome';
|
||||
import Page from './Page';
|
||||
|
||||
const enableNavigationAPI = typeof navigation === 'object';
|
||||
|
||||
export default function App({assets, initialURL}) {
|
||||
const [routerState, setRouterState] = useState({
|
||||
pendingNav: () => {},
|
||||
url: initialURL,
|
||||
});
|
||||
function navigate(url) {
|
||||
if (enableNavigationAPI) {
|
||||
window.navigation.navigate(url);
|
||||
} else {
|
||||
startTransition(() => {
|
||||
setRouterState({
|
||||
url,
|
||||
pendingNav() {
|
||||
window.history.pushState({}, '', url);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (enableNavigationAPI) {
|
||||
window.navigation.addEventListener('navigate', event => {
|
||||
if (!event.canIntercept) {
|
||||
return;
|
||||
}
|
||||
const newURL = new URL(event.destination.url);
|
||||
event.intercept({
|
||||
handler() {
|
||||
let promise;
|
||||
startTransition(() => {
|
||||
promise = new Promise(resolve => {
|
||||
setRouterState({
|
||||
url: newURL.pathname + newURL.search,
|
||||
pendingNav: resolve,
|
||||
});
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
commit: 'after-transition', // plz ship this, browsers
|
||||
});
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('popstate', () => {
|
||||
// This should not animate because restoration has to be synchronous.
|
||||
// Even though it's a transition.
|
||||
startTransition(() => {
|
||||
setRouterState({
|
||||
url: document.location.pathname + document.location.search,
|
||||
pendingNav() {
|
||||
// Noop. URL has already updated.
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const pendingNav = routerState.pendingNav;
|
||||
useLayoutEffect(() => {
|
||||
pendingNav();
|
||||
}, [pendingNav]);
|
||||
return (
|
||||
<Chrome title="Hello World" assets={assets}>
|
||||
<Page url={routerState.url} navigate={navigate} />
|
||||
</Chrome>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
body {
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import './Chrome.css';
|
||||
|
||||
export default class Chrome extends Component {
|
||||
render() {
|
||||
const assets = this.props.assets;
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link rel="stylesheet" href={assets['main.css']} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin=""
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>{this.props.title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<b>Enable JavaScript to run this app.</b>`,
|
||||
}}
|
||||
/>
|
||||
{this.props.children}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.roboto-font {
|
||||
font-family: "Roboto", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
"wdth" 100;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, {
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_Activity as Activity,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from 'react';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
import transitions from './Transitions.module.css';
|
||||
|
||||
const a = (
|
||||
<div key="a">
|
||||
<ViewTransition>
|
||||
<div>a</div>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
);
|
||||
|
||||
const b = (
|
||||
<div key="b">
|
||||
<ViewTransition>
|
||||
<div>b</div>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
);
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<ViewTransition
|
||||
className={
|
||||
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
|
||||
}>
|
||||
<p className="roboto-font">Slide In from Left, Slide Out to Right</p>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page({url, navigate}) {
|
||||
const ref = useRef();
|
||||
const show = url === '/?b';
|
||||
useLayoutEffect(() => {
|
||||
const viewTransition = ref.current;
|
||||
requestAnimationFrame(() => {
|
||||
const keyframes = [
|
||||
{rotate: '0deg', transformOrigin: '30px 8px'},
|
||||
{rotate: '360deg', transformOrigin: '30px 8px'},
|
||||
];
|
||||
viewTransition.old.animate(keyframes, 300);
|
||||
viewTransition.new.animate(keyframes, 300);
|
||||
});
|
||||
}, [show]);
|
||||
const exclamation = (
|
||||
<ViewTransition name="exclamation">
|
||||
<span>!</span>
|
||||
</ViewTransition>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(show ? '/?a' : '/?b');
|
||||
}}>
|
||||
{show ? 'A' : 'B'}
|
||||
</button>
|
||||
<ViewTransition>
|
||||
<div>
|
||||
{show ? (
|
||||
<div>
|
||||
{a}
|
||||
{b}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{b}
|
||||
{a}
|
||||
</div>
|
||||
)}
|
||||
<ViewTransition ref={ref}>
|
||||
{show ? <div>hello{exclamation}</div> : <section>Loading</section>}
|
||||
</ViewTransition>
|
||||
<p>scroll me</p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<p></p>
|
||||
{show ? null : (
|
||||
<ViewTransition>
|
||||
<div>world{exclamation}</div>
|
||||
</ViewTransition>
|
||||
)}
|
||||
<Activity mode={show ? 'visible' : 'hidden'}>
|
||||
<ViewTransition>
|
||||
<div>!!</div>
|
||||
</ViewTransition>
|
||||
</Activity>
|
||||
{show ? <Component /> : <p> </p>}
|
||||
</div>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@keyframes enter-slide-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
translate: -200px 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes exit-slide-left {
|
||||
0% {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
translate: 200px 0;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-new(.enter-slide-right):only-child {
|
||||
animation: enter-slide-right ease-in 0.25s;
|
||||
}
|
||||
::view-transition-old(.exit-slide-left):only-child {
|
||||
animation: exit-slide-left ease-in 0.25s;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import {hydrateRoot} from 'react-dom/client';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<App
|
||||
assets={window.assetManifest}
|
||||
initialURL={document.location.pathname + document.location.search}
|
||||
/>
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8300,6 +8300,7 @@ describe('rules-of-hooks/exhaustive-deps', () => {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
};
|
||||
|
||||
const languageOptionsV9 = {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
@@ -8442,7 +8443,7 @@ describe('rules-of-hooks/exhaustive-deps', () => {
|
||||
parser: require('@typescript-eslint/parser-v5'),
|
||||
},
|
||||
}).run(
|
||||
'eslint: v9, parser: @typescript-eslint/parser@^5.0.0-0',
|
||||
'eslint: v9, parser: @typescript-eslint/parser@^5.0.0',
|
||||
ReactHooksESLintRule,
|
||||
{
|
||||
valid: [
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
const ESLintTesterV7 = require('eslint-v7').RuleTester;
|
||||
const ESLintTesterV9 = require('eslint-v9').RuleTester;
|
||||
const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
|
||||
const BabelEslintParser = require('@babel/eslint-parser');
|
||||
const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks'];
|
||||
|
||||
/**
|
||||
@@ -1561,19 +1560,117 @@ if (!process.env.CI) {
|
||||
}
|
||||
|
||||
describe('rules-of-hooks/rules-of-hooks', () => {
|
||||
const parserOptionsV7 = {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
};
|
||||
|
||||
const languageOptionsV9 = {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('babel-eslint'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
},
|
||||
}).run('eslint: v7', ReactHooksESLintRule, tests);
|
||||
parserOptions: parserOptionsV7,
|
||||
}).run('eslint: v7, parser: babel-eslint', ReactHooksESLintRule, tests);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
parser: BabelEslintParser,
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
...languageOptionsV9,
|
||||
parser: require('@babel/eslint-parser'),
|
||||
},
|
||||
}).run('eslint: v9', ReactHooksESLintRule, tests);
|
||||
}).run(
|
||||
'eslint: v9, parser: @babel/eslint-parser',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('@typescript-eslint/parser-v2'),
|
||||
parserOptions: parserOptionsV7,
|
||||
}).run(
|
||||
'eslint: v7, parser: @typescript-eslint/parser@2.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
...languageOptionsV9,
|
||||
parser: require('@typescript-eslint/parser-v2'),
|
||||
},
|
||||
}).run(
|
||||
'eslint: v9, parser: @typescript-eslint/parser@2.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('@typescript-eslint/parser-v3'),
|
||||
parserOptions: parserOptionsV7,
|
||||
}).run(
|
||||
'eslint: v7, parser: @typescript-eslint/parser@3.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
...languageOptionsV9,
|
||||
parser: require('@typescript-eslint/parser-v3'),
|
||||
},
|
||||
}).run(
|
||||
'eslint: v9, parser: @typescript-eslint/parser@3.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('@typescript-eslint/parser-v4'),
|
||||
parserOptions: parserOptionsV7,
|
||||
}).run(
|
||||
'eslint: v7, parser: @typescript-eslint/parser@4.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
...languageOptionsV9,
|
||||
parser: require('@typescript-eslint/parser-v4'),
|
||||
},
|
||||
}).run(
|
||||
'eslint: v9, parser: @typescript-eslint/parser@4.x',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('@typescript-eslint/parser-v5'),
|
||||
parserOptions: parserOptionsV7,
|
||||
}).run(
|
||||
'eslint: v7, parser: @typescript-eslint/parser@^5.0.0-0',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
...languageOptionsV9,
|
||||
parser: require('@typescript-eslint/parser-v5'),
|
||||
},
|
||||
}).run(
|
||||
'eslint: v9, parser: @typescript-eslint/parser@^5.0.0',
|
||||
ReactHooksESLintRule,
|
||||
tests
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@typescript-eslint/parser-v2": "npm:@typescript-eslint/parser@^2.26.0",
|
||||
"@typescript-eslint/parser-v3": "npm:@typescript-eslint/parser@^3.10.0",
|
||||
"@typescript-eslint/parser-v4": "npm:@typescript-eslint/parser@^4.1.0",
|
||||
"@typescript-eslint/parser-v5": "npm:@typescript-eslint/parser@^5.0.0-0",
|
||||
"@typescript-eslint/parser-v5": "npm:@typescript-eslint/parser@^5.62.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint-v7": "npm:eslint@^7.7.0",
|
||||
"eslint-v9": "npm:eslint@^9.0.0"
|
||||
|
||||
@@ -549,7 +549,8 @@ export default {
|
||||
} else if (
|
||||
codePathNode.parent &&
|
||||
(codePathNode.parent.type === 'MethodDefinition' ||
|
||||
codePathNode.parent.type === 'ClassProperty') &&
|
||||
codePathNode.parent.type === 'ClassProperty' ||
|
||||
codePathNode.parent.type === 'PropertyDefinition') &&
|
||||
codePathNode.parent.value === codePathNode
|
||||
) {
|
||||
// Custom message for hooks inside a class
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
const React = require('react');
|
||||
const stripAnsi = require('strip-ansi');
|
||||
const {startTransition, useDeferredValue} = React;
|
||||
const chalk = require('chalk');
|
||||
const ReactNoop = require('react-noop-renderer');
|
||||
const {
|
||||
waitFor,
|
||||
@@ -25,7 +24,7 @@ const {
|
||||
const act = require('internal-test-utils').act;
|
||||
const Scheduler = require('scheduler/unstable_mock');
|
||||
const {
|
||||
flushAllUnexpectedConsoleCalls,
|
||||
assertConsoleLogsCleared,
|
||||
resetAllUnexpectedConsoleCalls,
|
||||
patchConsoleMethods,
|
||||
} = require('../consoleMock');
|
||||
@@ -205,16 +204,17 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
it('should fail if not asserted', () => {
|
||||
expect(() => {
|
||||
console.log('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toThrow(`Expected test not to call ${chalk.bold('console.log()')}.`);
|
||||
assertConsoleLogsCleared();
|
||||
}).toThrow(`console.log was called without assertConsoleLogDev`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail if mocked with spyOnDev', () => {
|
||||
spyOnDev(console, 'log').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.log('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
if (__DEV__) {
|
||||
console.log('hit');
|
||||
}
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnProd(console, 'log').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.log('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -231,33 +231,26 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnDevAndProd(console, 'log').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.log('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail with toLogDev', () => {
|
||||
expect(() => {
|
||||
console.log('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toLogDev(['hit']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('console.warn', () => {
|
||||
it('should fail if not asserted', () => {
|
||||
expect(() => {
|
||||
console.warn('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toThrow(`Expected test not to call ${chalk.bold('console.warn()')}.`);
|
||||
assertConsoleLogsCleared();
|
||||
}).toThrow('console.warn was called without assertConsoleWarnDev');
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail if mocked with spyOnDev', () => {
|
||||
spyOnDev(console, 'warn').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.warn('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
if (__DEV__) {
|
||||
console.warn('hit');
|
||||
}
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -266,7 +259,7 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnProd(console, 'warn').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.warn('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -274,33 +267,26 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnDevAndProd(console, 'warn').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.warn('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail with toWarnDev', () => {
|
||||
expect(() => {
|
||||
console.warn('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toWarnDev(['hit'], {withoutStack: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('console.error', () => {
|
||||
it('should fail if console.error is not asserted', () => {
|
||||
expect(() => {
|
||||
console.error('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toThrow(`Expected test not to call ${chalk.bold('console.error()')}.`);
|
||||
assertConsoleLogsCleared();
|
||||
}).toThrow('console.error was called without assertConsoleErrorDev');
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail if mocked with spyOnDev', () => {
|
||||
spyOnDev(console, 'error').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.error('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
if (__DEV__) {
|
||||
console.error('hit');
|
||||
}
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -309,7 +295,7 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnProd(console, 'error').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.error('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -317,17 +303,9 @@ describe('ReactInternalTestUtils console mocks', () => {
|
||||
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
|
||||
expect(() => {
|
||||
console.error('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
assertConsoleLogsCleared();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('should not fail with toErrorDev', () => {
|
||||
expect(() => {
|
||||
console.error('hit');
|
||||
flushAllUnexpectedConsoleCalls();
|
||||
}).toErrorDev(['hit'], {withoutStack: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,17 +339,19 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
|
||||
describe('assertConsoleLogDev', () => {
|
||||
// @gate __DEV__
|
||||
it('passes for a single log', () => {
|
||||
console.log('Hello');
|
||||
if (__DEV__) {
|
||||
console.log('Hello');
|
||||
}
|
||||
assertConsoleLogDev(['Hello']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('passes for multiple logs', () => {
|
||||
console.log('Hello');
|
||||
console.log('Good day');
|
||||
console.log('Bye');
|
||||
if (__DEV__) {
|
||||
console.log('Hello');
|
||||
console.log('Good day');
|
||||
console.log('Bye');
|
||||
}
|
||||
assertConsoleLogDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
|
||||
@@ -906,17 +886,19 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
|
||||
describe('assertConsoleWarnDev', () => {
|
||||
// @gate __DEV__
|
||||
it('passes if an warning contains a stack', () => {
|
||||
console.warn('Hello\n in div');
|
||||
if (__DEV__) {
|
||||
console.warn('Hello\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('passes if all warnings contain a stack', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
if (__DEV__) {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
|
||||
@@ -1353,14 +1335,17 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
|
||||
describe('global withoutStack', () => {
|
||||
// @gate __DEV__
|
||||
it('passes if warnings without stack explicitly opt out', () => {
|
||||
console.warn('Hello');
|
||||
if (__DEV__) {
|
||||
console.warn('Hello');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello'], {withoutStack: true});
|
||||
|
||||
console.warn('Hello');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye');
|
||||
if (__DEV__) {
|
||||
console.warn('Hello');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye');
|
||||
}
|
||||
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {
|
||||
withoutStack: true,
|
||||
@@ -1460,11 +1445,12 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
});
|
||||
describe('local withoutStack', () => {
|
||||
// @gate __DEV__
|
||||
it('passes when expected withoutStack logs matches the actual logs', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye\n in div');
|
||||
if (__DEV__) {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev([
|
||||
'Hello',
|
||||
['Good day', {withoutStack: true}],
|
||||
@@ -1981,17 +1967,19 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
|
||||
describe('assertConsoleErrorDev', () => {
|
||||
// @gate __DEV__
|
||||
it('passes if an error contains a stack', () => {
|
||||
console.error('Hello\n in div');
|
||||
if (__DEV__) {
|
||||
console.error('Hello\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('passes if all errors contain a stack', () => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
if (__DEV__) {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
|
||||
@@ -2446,14 +2434,17 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
|
||||
describe('global withoutStack', () => {
|
||||
// @gate __DEV__
|
||||
it('passes if errors without stack explicitly opt out', () => {
|
||||
console.error('Hello');
|
||||
if (__DEV__) {
|
||||
console.error('Hello');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello'], {withoutStack: true});
|
||||
|
||||
console.error('Hello');
|
||||
console.error('Good day');
|
||||
console.error('Bye');
|
||||
if (__DEV__) {
|
||||
console.error('Hello');
|
||||
console.error('Good day');
|
||||
console.error('Bye');
|
||||
}
|
||||
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {
|
||||
withoutStack: true,
|
||||
@@ -2553,11 +2544,12 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
});
|
||||
});
|
||||
describe('local withoutStack', () => {
|
||||
// @gate __DEV__
|
||||
it('passes when expected withoutStack logs matches the actual logs', () => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day');
|
||||
console.error('Bye\n in div');
|
||||
if (__DEV__) {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day');
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev([
|
||||
'Hello',
|
||||
['Good day', {withoutStack: true}],
|
||||
|
||||
@@ -19,19 +19,7 @@ const loggedErrors = (global.__loggedErrors = global.__loggedErrors || []);
|
||||
const loggedWarns = (global.__loggedWarns = global.__loggedWarns || []);
|
||||
const loggedLogs = (global.__loggedLogs = global.__loggedLogs || []);
|
||||
|
||||
// TODO: delete these after code modding away from toWarnDev.
|
||||
const unexpectedErrorCallStacks = (global.__unexpectedErrorCallStacks =
|
||||
global.__unexpectedErrorCallStacks || []);
|
||||
const unexpectedWarnCallStacks = (global.__unexpectedWarnCallStacks =
|
||||
global.__unexpectedWarnCallStacks || []);
|
||||
const unexpectedLogCallStacks = (global.__unexpectedLogCallStacks =
|
||||
global.__unexpectedLogCallStacks || []);
|
||||
|
||||
const patchConsoleMethod = (
|
||||
methodName,
|
||||
unexpectedConsoleCallStacks,
|
||||
logged,
|
||||
) => {
|
||||
const patchConsoleMethod = (methodName, logged) => {
|
||||
const newMethod = function (format, ...args) {
|
||||
// Ignore uncaught errors reported by jsdom
|
||||
// and React addendums because they're too noisy.
|
||||
@@ -72,14 +60,6 @@ const patchConsoleMethod = (
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the call stack now so we can warn about it later.
|
||||
// The call stack has helpful information for the test author.
|
||||
// Don't throw yet though b'c it might be accidentally caught and suppressed.
|
||||
const stack = new Error().stack;
|
||||
unexpectedConsoleCallStacks.push([
|
||||
stack.slice(stack.indexOf('\n') + 1),
|
||||
util.format(format, ...args),
|
||||
]);
|
||||
logged.push([format, ...args]);
|
||||
};
|
||||
|
||||
@@ -88,123 +68,40 @@ const patchConsoleMethod = (
|
||||
return newMethod;
|
||||
};
|
||||
|
||||
const flushUnexpectedConsoleCalls = (
|
||||
mockMethod,
|
||||
methodName,
|
||||
expectedMatcher,
|
||||
unexpectedConsoleCallStacks,
|
||||
) => {
|
||||
if (
|
||||
console[methodName] !== mockMethod &&
|
||||
!jest.isMockFunction(console[methodName])
|
||||
) {
|
||||
// throw new Error(
|
||||
// `Test did not tear down console.${methodName} mock properly.`
|
||||
// );
|
||||
}
|
||||
if (unexpectedConsoleCallStacks.length > 0) {
|
||||
const messages = unexpectedConsoleCallStacks.map(
|
||||
([stack, message]) =>
|
||||
`${chalk.red(message)}\n` +
|
||||
`${stack
|
||||
.split('\n')
|
||||
.map(line => chalk.gray(line))
|
||||
.join('\n')}`,
|
||||
);
|
||||
|
||||
const type = methodName === 'log' ? 'log' : 'warning';
|
||||
const message =
|
||||
`Expected test not to call ${chalk.bold(
|
||||
`console.${methodName}()`,
|
||||
)}.\n\n` +
|
||||
`If the ${type} is expected, test for it explicitly by:\n` +
|
||||
`1. Using ${chalk.bold(expectedMatcher + '()')} or...\n` +
|
||||
`2. Mock it out using ${chalk.bold(
|
||||
'spyOnDev',
|
||||
)}(console, '${methodName}') or ${chalk.bold(
|
||||
'spyOnProd',
|
||||
)}(console, '${methodName}'), and test that the ${type} occurs.`;
|
||||
|
||||
throw new Error(`${message}\n\n${messages.join('\n\n')}`);
|
||||
}
|
||||
};
|
||||
|
||||
let errorMethod;
|
||||
let warnMethod;
|
||||
let logMethod;
|
||||
export function patchConsoleMethods({includeLog} = {includeLog: false}) {
|
||||
errorMethod = patchConsoleMethod(
|
||||
'error',
|
||||
unexpectedErrorCallStacks,
|
||||
loggedErrors,
|
||||
);
|
||||
warnMethod = patchConsoleMethod(
|
||||
'warn',
|
||||
unexpectedWarnCallStacks,
|
||||
loggedWarns,
|
||||
);
|
||||
patchConsoleMethod('error', loggedErrors);
|
||||
patchConsoleMethod('warn', loggedWarns);
|
||||
|
||||
// Only assert console.log isn't called in CI so you can debug tests in DEV.
|
||||
// The matchers will still work in DEV, so you can assert locally.
|
||||
if (includeLog) {
|
||||
logMethod = patchConsoleMethod('log', unexpectedLogCallStacks, loggedLogs);
|
||||
logMethod = patchConsoleMethod('log', loggedLogs);
|
||||
}
|
||||
}
|
||||
|
||||
export function flushAllUnexpectedConsoleCalls() {
|
||||
flushUnexpectedConsoleCalls(
|
||||
errorMethod,
|
||||
'error',
|
||||
'assertConsoleErrorDev',
|
||||
unexpectedErrorCallStacks,
|
||||
);
|
||||
flushUnexpectedConsoleCalls(
|
||||
warnMethod,
|
||||
'warn',
|
||||
'assertConsoleWarnDev',
|
||||
unexpectedWarnCallStacks,
|
||||
);
|
||||
if (logMethod) {
|
||||
flushUnexpectedConsoleCalls(
|
||||
logMethod,
|
||||
'log',
|
||||
'assertConsoleLogDev',
|
||||
unexpectedLogCallStacks,
|
||||
);
|
||||
unexpectedLogCallStacks.length = 0;
|
||||
}
|
||||
unexpectedErrorCallStacks.length = 0;
|
||||
unexpectedWarnCallStacks.length = 0;
|
||||
}
|
||||
|
||||
export function resetAllUnexpectedConsoleCalls() {
|
||||
loggedErrors.length = 0;
|
||||
loggedWarns.length = 0;
|
||||
unexpectedErrorCallStacks.length = 0;
|
||||
unexpectedWarnCallStacks.length = 0;
|
||||
if (logMethod) {
|
||||
loggedLogs.length = 0;
|
||||
unexpectedLogCallStacks.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearLogs() {
|
||||
const logs = Array.from(loggedLogs);
|
||||
unexpectedLogCallStacks.length = 0;
|
||||
loggedLogs.length = 0;
|
||||
return logs;
|
||||
}
|
||||
|
||||
export function clearWarnings() {
|
||||
const warnings = Array.from(loggedWarns);
|
||||
unexpectedWarnCallStacks.length = 0;
|
||||
loggedWarns.length = 0;
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export function clearErrors() {
|
||||
const errors = Array.from(loggedErrors);
|
||||
unexpectedErrorCallStacks.length = 0;
|
||||
loggedErrors.length = 0;
|
||||
return errors;
|
||||
}
|
||||
|
||||
+55
@@ -455,6 +455,59 @@ export function unhideTextInstance(textInstance, text): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function applyViewTransitionName(instance, name, className) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function restoreViewTransitionName(instance, props) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function cancelViewTransitionName(instance, name, props) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function cancelRootViewTransitionName(rootContainer) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function restoreRootViewTransitionName(rootContainer) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export type InstanceMeasurement = null;
|
||||
|
||||
export function measureInstance(instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function wasInstanceInViewport(measurement): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function hasInstanceChanged(oldMeasurement, newMeasurement): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasInstanceAffectedParent(
|
||||
oldMeasurement,
|
||||
newMeasurement,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function startViewTransition() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
export function createViewTransitionInstance(
|
||||
name: string,
|
||||
): ViewTransitionInstance {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function clearContainer(container) {
|
||||
// TODO Implement this
|
||||
}
|
||||
@@ -497,6 +550,8 @@ export function startSuspendingCommit() {}
|
||||
|
||||
export function suspendInstance(type, props) {}
|
||||
|
||||
export function suspendOnActiveViewTransition(container) {}
|
||||
|
||||
export function waitForCommitToBeReady() {
|
||||
return null;
|
||||
}
|
||||
|
||||
+43
-39
@@ -45,7 +45,6 @@ import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
|
||||
import {
|
||||
enablePostpone,
|
||||
enableOwnerStacks,
|
||||
enableServerComponentLogs,
|
||||
enableProfilerTimer,
|
||||
enableComponentPerformanceTrack,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
@@ -73,6 +72,7 @@ import {
|
||||
markAllTracksInOrder,
|
||||
logComponentRender,
|
||||
logDedupedComponentRender,
|
||||
logComponentErrored,
|
||||
} from './ReactFlightPerformanceTrack';
|
||||
|
||||
import {
|
||||
@@ -2138,34 +2138,22 @@ function resolveErrorDev(
|
||||
}
|
||||
|
||||
let error;
|
||||
if (!enableOwnerStacks && !enableServerComponentLogs) {
|
||||
// Executing Error within a native stack isn't really limited to owner stacks
|
||||
// but we gate it behind the same flag for now while iterating.
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
error = Error(
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
env,
|
||||
// $FlowFixMe[incompatible-use]
|
||||
Error.bind(
|
||||
null,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
);
|
||||
// For backwards compat we use the V8 formatting when the flag is off.
|
||||
error.stack = formatV8Stack(error.name, error.message, stack);
|
||||
),
|
||||
);
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
} else {
|
||||
const callStack = buildFakeCallStack(
|
||||
response,
|
||||
stack,
|
||||
env,
|
||||
// $FlowFixMe[incompatible-use]
|
||||
Error.bind(
|
||||
null,
|
||||
message ||
|
||||
'An error occurred in the Server Components render but no message was provided',
|
||||
),
|
||||
);
|
||||
const rootTask = getRootTask(response, env);
|
||||
if (rootTask != null) {
|
||||
error = rootTask.run(callStack);
|
||||
} else {
|
||||
error = callStack();
|
||||
}
|
||||
error = callStack();
|
||||
}
|
||||
|
||||
(error: any).environmentName = env;
|
||||
@@ -2698,11 +2686,6 @@ function resolveConsoleEntry(
|
||||
const env = payload[3];
|
||||
const args = payload.slice(4);
|
||||
|
||||
if (!enableOwnerStacks && !enableServerComponentLogs) {
|
||||
bindToConsole(methodName, args, env)();
|
||||
return;
|
||||
}
|
||||
|
||||
replayConsoleWithCallStackInDEV(
|
||||
response,
|
||||
methodName,
|
||||
@@ -2876,6 +2859,7 @@ function flushComponentPerformance(
|
||||
|
||||
if (debugInfo) {
|
||||
let endTime = 0;
|
||||
let isLastComponent = true;
|
||||
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
||||
const info = debugInfo[i];
|
||||
if (typeof info.time === 'number') {
|
||||
@@ -2890,17 +2874,37 @@ function flushComponentPerformance(
|
||||
const startTimeInfo = debugInfo[i - 1];
|
||||
if (typeof startTimeInfo.time === 'number') {
|
||||
const startTime = startTimeInfo.time;
|
||||
logComponentRender(
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
endTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
if (
|
||||
isLastComponent &&
|
||||
root.status === ERRORED &&
|
||||
root.reason !== response._closedReason
|
||||
) {
|
||||
// If this is the last component to render before this chunk rejected, then conceptually
|
||||
// this component errored. If this was a cancellation then it wasn't this component that
|
||||
// errored.
|
||||
logComponentErrored(
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
endTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
root.reason,
|
||||
);
|
||||
} else {
|
||||
logComponentRender(
|
||||
componentInfo,
|
||||
trackIdx,
|
||||
startTime,
|
||||
endTime,
|
||||
childrenEndTime,
|
||||
response._rootEnvironmentName,
|
||||
);
|
||||
}
|
||||
// Track the root most component of the result for deduping logging.
|
||||
result.component = componentInfo;
|
||||
}
|
||||
isLastComponent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,49 @@ export function logComponentRender(
|
||||
}
|
||||
}
|
||||
|
||||
export function logComponentErrored(
|
||||
componentInfo: ReactComponentInfo,
|
||||
trackIdx: number,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
childrenEndTime: number,
|
||||
rootEnv: string,
|
||||
error: mixed,
|
||||
): void {
|
||||
if (supportsUserTiming) {
|
||||
const properties = [];
|
||||
if (__DEV__) {
|
||||
const message =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
typeof error.message === 'string'
|
||||
? // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error.message)
|
||||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
properties.push(['Error', message]);
|
||||
}
|
||||
const env = componentInfo.env;
|
||||
const name = componentInfo.name;
|
||||
const isPrimaryEnv = env === rootEnv;
|
||||
const entryName =
|
||||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
performance.measure(entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: 'error',
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
tooltipText: entryName + ' Errored',
|
||||
properties,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function logDedupedComponentRender(
|
||||
componentInfo: ReactComponentInfo,
|
||||
trackIdx: number,
|
||||
|
||||
+23
-44
@@ -1377,26 +1377,14 @@ describe('ReactFlight', () => {
|
||||
errors: [
|
||||
{
|
||||
message: 'This is an error',
|
||||
stack: gate(
|
||||
flags =>
|
||||
flags.enableOwnerStacks || flags.enableServerComponentLogs,
|
||||
)
|
||||
? expect.stringContaining(
|
||||
'Error: This is an error\n' +
|
||||
' at eval (eval at testFunction (inspected-page.html:29:11),%20%3Canonymous%3E:1:35)\n' +
|
||||
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
|
||||
' at <anonymous> (file:///testing.js:42:3)\n' +
|
||||
' at <anonymous> (file:///testing.js:42:3)\n' +
|
||||
' at div (<anonymous>',
|
||||
)
|
||||
: expect.stringContaining(
|
||||
'Error: This is an error\n' +
|
||||
' at eval (eval at testFunction (inspected-page.html:29:11),%20%3Canonymous%3E:1:10)\n' +
|
||||
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
|
||||
' at file:///testing.js:42:3\n' +
|
||||
' at file:///testing.js:42:3\n' +
|
||||
' at div (<anonymous>',
|
||||
),
|
||||
stack: expect.stringContaining(
|
||||
'Error: This is an error\n' +
|
||||
' at eval (eval at testFunction (inspected-page.html:29:11),%20%3Canonymous%3E:1:35)\n' +
|
||||
' at ServerComponentError (file://~/(some)(really)(exotic-directory)/ReactFlight-test.js:1166:19)\n' +
|
||||
' at <anonymous> (file:///testing.js:42:3)\n' +
|
||||
' at <anonymous> (file:///testing.js:42:3)\n' +
|
||||
' at div (<anonymous>',
|
||||
),
|
||||
digest: 'a dev digest',
|
||||
environmentName: 'Server',
|
||||
},
|
||||
@@ -1415,18 +1403,16 @@ describe('ReactFlight', () => {
|
||||
['', 'Server'],
|
||||
[__filename, 'Server'],
|
||||
]
|
||||
: gate(flags => flags.enableServerComponentLogs)
|
||||
? [
|
||||
// TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
|
||||
['inspected-page.html:29:11), <anonymous>', 'Server'],
|
||||
[
|
||||
'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
|
||||
'Server',
|
||||
],
|
||||
['file:///testing.js', 'Server'],
|
||||
['', 'Server'],
|
||||
]
|
||||
: [],
|
||||
: [
|
||||
// TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
|
||||
['inspected-page.html:29:11), <anonymous>', 'Server'],
|
||||
[
|
||||
'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
|
||||
'Server',
|
||||
],
|
||||
['file:///testing.js', 'Server'],
|
||||
['', 'Server'],
|
||||
],
|
||||
});
|
||||
} else {
|
||||
expect(errors.map(getErrorForJestMatcher)).toEqual([
|
||||
@@ -3312,14 +3298,7 @@ describe('ReactFlight', () => {
|
||||
.split('\n')
|
||||
.slice(0, 4)
|
||||
.join('\n')
|
||||
.replaceAll(
|
||||
' (/',
|
||||
gate(
|
||||
flags => flags.enableOwnerStacks || flags.enableServerComponentLogs,
|
||||
)
|
||||
? ' (file:///'
|
||||
: ' (/',
|
||||
); // The eval will end up normalizing these
|
||||
.replaceAll(' (/', ' (file:///'); // The eval will end up normalizing these
|
||||
|
||||
let sawReactPrefix = false;
|
||||
const environments = [];
|
||||
@@ -3352,7 +3331,7 @@ describe('ReactFlight', () => {
|
||||
'third-party',
|
||||
'third-party',
|
||||
]);
|
||||
} else if (__DEV__ && gate(flags => flags.enableServerComponentLogs)) {
|
||||
} else if (__DEV__) {
|
||||
expect(environments.slice(0, 3)).toEqual([
|
||||
'third-party',
|
||||
'third-party',
|
||||
@@ -3412,7 +3391,7 @@ describe('ReactFlight', () => {
|
||||
expect(ReactNoop).toMatchRenderedOutput(<div>hi</div>);
|
||||
});
|
||||
|
||||
// @gate enableServerComponentLogs && __DEV__ && enableOwnerStacks
|
||||
// @gate __DEV__ && enableOwnerStacks
|
||||
it('replays logs, but not onError logs', async () => {
|
||||
function foo() {
|
||||
return 'hello';
|
||||
@@ -3493,7 +3472,7 @@ describe('ReactFlight', () => {
|
||||
expect(ownerStacks).toEqual(['\n in App (at **)']);
|
||||
});
|
||||
|
||||
// @gate enableServerComponentLogs && __DEV__
|
||||
// @gate __DEV__
|
||||
it('replays logs with cyclic objects', async () => {
|
||||
const cyclic = {cycle: null};
|
||||
cyclic.cycle = cyclic;
|
||||
@@ -3764,7 +3743,7 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__
|
||||
// @gate (enableOwnerStacks) || !__DEV__
|
||||
it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => {
|
||||
class MyError extends Error {
|
||||
toJSON() {
|
||||
|
||||
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
|
||||
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = false;
|
||||
|
||||
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
|
||||
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
|
||||
export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
|
||||
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
|
||||
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
|
||||
export * from 'react-client/src/ReactClientConsoleConfigServer';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
|
||||
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
|
||||
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||
export const usedWithSSR = true;
|
||||
|
||||
+6
-4
@@ -206,8 +206,12 @@ function createComponentsPanel() {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: we should listen to createdPanel.onHidden to unmount some listeners
|
||||
// and potentially stop highlighting
|
||||
createdPanel.onShown.addListener(() => {
|
||||
bridge.emit('extensionComponentsPanelShown');
|
||||
});
|
||||
createdPanel.onHidden.addListener(() => {
|
||||
bridge.emit('extensionComponentsPanelHidden');
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -345,8 +349,6 @@ function mountReactDevTools() {
|
||||
|
||||
createBridgeAndStore();
|
||||
|
||||
setReactSelectionFromBrowser(bridge);
|
||||
|
||||
createComponentsPanel();
|
||||
createProfilerPanel();
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ test.describe('Components', () => {
|
||||
runOnlyForReactRange('>=16.8');
|
||||
|
||||
// Select the first list item in DevTools.
|
||||
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
|
||||
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true);
|
||||
|
||||
// Then read the inspected values.
|
||||
const sourceText = await page.evaluate(() => {
|
||||
@@ -127,7 +127,7 @@ test.describe('Components', () => {
|
||||
const container = document.getElementById('devtools');
|
||||
|
||||
const source = findAllNodes(container, [
|
||||
createTestNameSelector('InspectedElementView-Source'),
|
||||
createTestNameSelector('InspectedElementView-FormattedSourceString'),
|
||||
])[0];
|
||||
|
||||
return source.innerText;
|
||||
@@ -237,35 +237,35 @@ test.describe('Components', () => {
|
||||
}
|
||||
|
||||
await focusComponentSearch();
|
||||
page.keyboard.insertText('List');
|
||||
await page.keyboard.insertText('List');
|
||||
let count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('1 | 4');
|
||||
|
||||
page.keyboard.insertText('Item');
|
||||
await page.keyboard.insertText('Item');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('1 | 3');
|
||||
|
||||
page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('2 | 3');
|
||||
|
||||
page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('3 | 3');
|
||||
|
||||
page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('1 | 3');
|
||||
|
||||
page.keyboard.press('Shift+Enter');
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('3 | 3');
|
||||
|
||||
page.keyboard.press('Shift+Enter');
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('2 | 3');
|
||||
|
||||
page.keyboard.press('Shift+Enter');
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
count = await getComponentSearchResultsCount();
|
||||
expect(count).toBe('1 | 3');
|
||||
});
|
||||
|
||||
@@ -27,7 +27,12 @@ async function getElementCount(page, displayName) {
|
||||
}, displayName);
|
||||
}
|
||||
|
||||
async function selectElement(page, displayName, waitForOwnersText) {
|
||||
async function selectElement(
|
||||
page,
|
||||
displayName,
|
||||
waitForOwnersText,
|
||||
waitForSourceLoaded = false
|
||||
) {
|
||||
await page.evaluate(listItemText => {
|
||||
const {createTestNameSelector, createTextSelector, findAllNodes} =
|
||||
window.REACT_DOM_DEVTOOLS;
|
||||
@@ -69,6 +74,20 @@ async function selectElement(page, displayName, waitForOwnersText) {
|
||||
{titleText: displayName, ownersListText: waitForOwnersText}
|
||||
);
|
||||
}
|
||||
|
||||
if (waitForSourceLoaded) {
|
||||
await page.waitForFunction(() => {
|
||||
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
|
||||
const container = document.getElementById('devtools');
|
||||
|
||||
const sourceStringBlock = findAllNodes(container, [
|
||||
createTestNameSelector('InspectedElementView-FormattedSourceString'),
|
||||
])[0];
|
||||
|
||||
// Wait for a new source line to be fetched
|
||||
return sourceStringBlock != null && sourceStringBlock.innerText != null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
+76
-1
@@ -186,8 +186,83 @@ describe('Fast Refresh', () => {
|
||||
expect(getContainer().firstChild).not.toBe(element);
|
||||
});
|
||||
|
||||
// @reactVersion < 18.0
|
||||
// @reactVersion >= 16.9
|
||||
it('should not break when there are warnings in between patching', () => {
|
||||
it('should not break when there are warnings in between patching (before post commit hook)', () => {
|
||||
withErrorsOrWarningsIgnored(['Expected:'], () => {
|
||||
render(`
|
||||
const {useState} = React;
|
||||
|
||||
export default function Component() {
|
||||
const [state, setState] = useState(1);
|
||||
console.warn("Expected: warning during render");
|
||||
return null;
|
||||
}
|
||||
`);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 1
|
||||
[root]
|
||||
<Component> ⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['Expected:'], () => {
|
||||
patch(`
|
||||
const {useEffect, useState} = React;
|
||||
|
||||
export default function Component() {
|
||||
const [state, setState] = useState(1);
|
||||
console.warn("Expected: warning during render");
|
||||
return null;
|
||||
}
|
||||
`);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 2
|
||||
[root]
|
||||
<Component> ⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['Expected:'], () => {
|
||||
patch(`
|
||||
const {useEffect, useState} = React;
|
||||
|
||||
export default function Component() {
|
||||
const [state, setState] = useState(1);
|
||||
useEffect(() => {
|
||||
console.error("Expected: error during effect");
|
||||
});
|
||||
console.warn("Expected: warning during render");
|
||||
return null;
|
||||
}
|
||||
`);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 1
|
||||
[root]
|
||||
<Component> ⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['Expected:'], () => {
|
||||
patch(`
|
||||
const {useEffect, useState} = React;
|
||||
|
||||
export default function Component() {
|
||||
const [state, setState] = useState(1);
|
||||
console.warn("Expected: warning during render");
|
||||
return null;
|
||||
}
|
||||
`);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 1
|
||||
[root]
|
||||
<Component> ⚠
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('should not break when there are warnings in between patching (with post commit hook)', () => {
|
||||
withErrorsOrWarningsIgnored(['Expected:'], () => {
|
||||
render(`
|
||||
const {useState} = React;
|
||||
|
||||
@@ -115,16 +115,15 @@ describe('InspectedElement', () => {
|
||||
|
||||
const Contexts = ({
|
||||
children,
|
||||
defaultSelectedElementID = null,
|
||||
defaultSelectedElementIndex = null,
|
||||
defaultInspectedElementID = null,
|
||||
defaultInspectedElementIndex = null,
|
||||
}) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<SettingsContextController>
|
||||
<TreeContextController
|
||||
defaultSelectedElementID={defaultSelectedElementID}
|
||||
defaultSelectedElementIndex={defaultSelectedElementIndex}
|
||||
defaultInspectedElementID={defaultSelectedElementID}>
|
||||
defaultInspectedElementID={defaultInspectedElementID}
|
||||
defaultInspectedElementIndex={defaultInspectedElementIndex}>
|
||||
<InspectedElementContextController>
|
||||
{children}
|
||||
</InspectedElementContextController>
|
||||
@@ -167,8 +166,8 @@ describe('InspectedElement', () => {
|
||||
testRendererInstance.update(
|
||||
<ErrorBoundary>
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={index}>
|
||||
defaultInspectedElementID={id}
|
||||
defaultInspectedElementIndex={index}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender id={id} index={index} />
|
||||
</React.Suspense>
|
||||
@@ -355,7 +354,7 @@ describe('InspectedElement', () => {
|
||||
const {index, shouldHaveLegacyContext} = cases[i];
|
||||
|
||||
// HACK: Recreate TestRenderer instance because we rely on default state values
|
||||
// from props like defaultSelectedElementID and it's easier to reset here than
|
||||
// from props like defaultInspectedElementID and it's easier to reset here than
|
||||
// to read the TreeDispatcherContext and update the selected ID that way.
|
||||
// We're testing the inspected values here, not the context wiring, so that's ok.
|
||||
withErrorsOrWarningsIgnored(
|
||||
@@ -2069,7 +2068,7 @@ describe('InspectedElement', () => {
|
||||
}, false);
|
||||
|
||||
// HACK: Recreate TestRenderer instance because we rely on default state values
|
||||
// from props like defaultSelectedElementID and it's easier to reset here than
|
||||
// from props like defaultInspectedElementID and it's easier to reset here than
|
||||
// to read the TreeDispatcherContext and update the selected ID that way.
|
||||
// We're testing the inspected values here, not the context wiring, so that's ok.
|
||||
withErrorsOrWarningsIgnored(
|
||||
@@ -2129,7 +2128,7 @@ describe('InspectedElement', () => {
|
||||
}, false);
|
||||
|
||||
// HACK: Recreate TestRenderer instance because we rely on default state values
|
||||
// from props like defaultSelectedElementID and it's easier to reset here than
|
||||
// from props like defaultInspectedElementID and it's easier to reset here than
|
||||
// to read the TreeDispatcherContext and update the selected ID that way.
|
||||
// We're testing the inspected values here, not the context wiring, so that's ok.
|
||||
withErrorsOrWarningsIgnored(
|
||||
@@ -2408,8 +2407,8 @@ describe('InspectedElement', () => {
|
||||
await utils.actAsync(() => {
|
||||
root = TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={index}>
|
||||
defaultInspectedElementID={id}
|
||||
defaultInspectedElementIndex={index}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
@@ -3101,6 +3100,7 @@ describe('InspectedElement', () => {
|
||||
|
||||
await utils.actAsync(() => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
|
||||
jest.runOnlyPendingTimers();
|
||||
}, false);
|
||||
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
@@ -3120,6 +3120,7 @@ describe('InspectedElement', () => {
|
||||
|
||||
await utils.actAsync(() => {
|
||||
store.componentFilters = [];
|
||||
jest.runOnlyPendingTimers();
|
||||
}, false);
|
||||
expect(state).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 2
|
||||
|
||||
@@ -69,14 +69,14 @@ describe('ProfilerContext', () => {
|
||||
|
||||
const Contexts = ({
|
||||
children = null,
|
||||
defaultSelectedElementID = null,
|
||||
defaultSelectedElementIndex = null,
|
||||
defaultInspectedElementID = null,
|
||||
defaultInspectedElementIndex = null,
|
||||
}: any) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController
|
||||
defaultSelectedElementID={defaultSelectedElementID}
|
||||
defaultSelectedElementIndex={defaultSelectedElementIndex}>
|
||||
defaultInspectedElementID={defaultInspectedElementID}
|
||||
defaultInspectedElementIndex={defaultInspectedElementIndex}>
|
||||
<ProfilerContextController>{children}</ProfilerContextController>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
@@ -225,8 +225,8 @@ describe('ProfilerContext', () => {
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}>
|
||||
defaultInspectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
@@ -276,8 +276,8 @@ describe('ProfilerContext', () => {
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}>
|
||||
defaultInspectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
@@ -323,8 +323,8 @@ describe('ProfilerContext', () => {
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}>
|
||||
defaultInspectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
@@ -374,8 +374,8 @@ describe('ProfilerContext', () => {
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}>
|
||||
defaultInspectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
@@ -415,11 +415,12 @@ describe('ProfilerContext', () => {
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
|
||||
let selectedElementID = null;
|
||||
let inspectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
dispatch = React.useContext(TreeDispatcherContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
inspectedElementID =
|
||||
React.useContext(TreeStateContext).inspectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -428,13 +429,15 @@ describe('ProfilerContext', () => {
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
|
||||
<Contexts
|
||||
defaultInspectedElementID={id}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(selectedElementID).toBe(id);
|
||||
expect(inspectedElementID).toBe(id);
|
||||
|
||||
// Profile and record more updates to both roots
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
@@ -448,7 +451,7 @@ describe('ProfilerContext', () => {
|
||||
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
|
||||
|
||||
// Verify that the initial Profiler root selection is maintained.
|
||||
expect(selectedElementID).toBe(otherID);
|
||||
expect(inspectedElementID).toBe(otherID);
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(store.getRootIDForElement(id));
|
||||
});
|
||||
@@ -484,11 +487,12 @@ describe('ProfilerContext', () => {
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
|
||||
let selectedElementID = null;
|
||||
let inspectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
dispatch = React.useContext(TreeDispatcherContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
inspectedElementID =
|
||||
React.useContext(TreeStateContext).inspectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -497,13 +501,15 @@ describe('ProfilerContext', () => {
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
|
||||
<Contexts
|
||||
defaultInspectedElementID={id}
|
||||
defaultInspectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(selectedElementID).toBe(id);
|
||||
expect(inspectedElementID).toBe(id);
|
||||
|
||||
// Profile and record more updates to both roots
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
@@ -517,7 +523,7 @@ describe('ProfilerContext', () => {
|
||||
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
|
||||
|
||||
// Verify that the initial Profiler root selection is maintained.
|
||||
expect(selectedElementID).toBe(otherID);
|
||||
expect(inspectedElementID).toBe(otherID);
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(store.getRootIDForElement(id));
|
||||
});
|
||||
@@ -553,10 +559,11 @@ describe('ProfilerContext', () => {
|
||||
`);
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let selectedElementID = null;
|
||||
let inspectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
inspectedElementID =
|
||||
React.useContext(TreeStateContext).inspectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -567,14 +574,14 @@ describe('ProfilerContext', () => {
|
||||
</Contexts>,
|
||||
),
|
||||
);
|
||||
expect(selectedElementID).toBeNull();
|
||||
expect(inspectedElementID).toBeNull();
|
||||
|
||||
// Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
|
||||
await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
expect(inspectedElementID).toBe(parentID);
|
||||
|
||||
// Select an unmounted element and verify no Components tab selection doesn't change.
|
||||
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
expect(inspectedElementID).toBe(parentID);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,7 +148,6 @@ export default class Agent extends EventEmitter<{
|
||||
getIfHasUnsupportedRendererVersion: [],
|
||||
updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
|
||||
getHookSettings: [],
|
||||
showNamesWhenTracing: [boolean],
|
||||
}> {
|
||||
_bridge: BackendBridge;
|
||||
_isProfiling: boolean = false;
|
||||
@@ -159,7 +158,6 @@ export default class Agent extends EventEmitter<{
|
||||
_onReloadAndProfile:
|
||||
| ((recordChangeDescriptions: boolean, recordTimeline: boolean) => void)
|
||||
| void;
|
||||
_showNamesWhenTracing: boolean = true;
|
||||
|
||||
constructor(
|
||||
bridge: BackendBridge,
|
||||
@@ -204,7 +202,6 @@ export default class Agent extends EventEmitter<{
|
||||
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
|
||||
bridge.addListener('renamePath', this.renamePath);
|
||||
bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
|
||||
bridge.addListener('setShowNamesWhenTracing', this.setShowNamesWhenTracing);
|
||||
bridge.addListener('startProfiling', this.startProfiling);
|
||||
bridge.addListener('stopProfiling', this.stopProfiling);
|
||||
bridge.addListener('storeAsGlobal', this.storeAsGlobal);
|
||||
@@ -727,7 +724,6 @@ export default class Agent extends EventEmitter<{
|
||||
this._traceUpdatesEnabled = traceUpdatesEnabled;
|
||||
|
||||
setTraceUpdatesEnabled(traceUpdatesEnabled);
|
||||
this.emit('showNamesWhenTracing', this._showNamesWhenTracing);
|
||||
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
@@ -737,14 +733,6 @@ export default class Agent extends EventEmitter<{
|
||||
}
|
||||
};
|
||||
|
||||
setShowNamesWhenTracing: (show: boolean) => void = show => {
|
||||
if (this._showNamesWhenTracing === show) {
|
||||
return;
|
||||
}
|
||||
this._showNamesWhenTracing = show;
|
||||
this.emit('showNamesWhenTracing', show);
|
||||
};
|
||||
|
||||
syncSelectionFromBuiltinElementsPanel: () => void = () => {
|
||||
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
|
||||
if (target == null) {
|
||||
|
||||
+7
@@ -43,6 +43,7 @@ export function describeFiber(
|
||||
SimpleMemoComponent,
|
||||
ForwardRef,
|
||||
ClassComponent,
|
||||
ViewTransitionComponent,
|
||||
} = workTagMap;
|
||||
|
||||
switch (workInProgress.tag) {
|
||||
@@ -57,6 +58,8 @@ export function describeFiber(
|
||||
return describeBuiltInComponentFrame('Suspense');
|
||||
case SuspenseListComponent:
|
||||
return describeBuiltInComponentFrame('SuspenseList');
|
||||
case ViewTransitionComponent:
|
||||
return describeBuiltInComponentFrame('ViewTransition');
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
case SimpleMemoComponent:
|
||||
@@ -150,6 +153,7 @@ export function getOwnerStackByFiberInDev(
|
||||
HostComponent,
|
||||
SuspenseComponent,
|
||||
SuspenseListComponent,
|
||||
ViewTransitionComponent,
|
||||
} = workTagMap;
|
||||
try {
|
||||
let info = '';
|
||||
@@ -177,6 +181,9 @@ export function getOwnerStackByFiberInDev(
|
||||
case SuspenseListComponent:
|
||||
info += describeBuiltInComponentFrame('SuspenseList');
|
||||
break;
|
||||
case ViewTransitionComponent:
|
||||
info += describeBuiltInComponentFrame('ViewTransition');
|
||||
break;
|
||||
}
|
||||
|
||||
let owner: void | null | Fiber | ReactComponentInfo = workInProgress;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ElementTypeSuspense,
|
||||
ElementTypeSuspenseList,
|
||||
ElementTypeTracingMarker,
|
||||
ElementTypeViewTransition,
|
||||
ElementTypeVirtual,
|
||||
StrictMode,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -383,6 +384,7 @@ export function getInternalReactConstants(version: string): {
|
||||
// want to fork again so we're adding it here instead
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: 29,
|
||||
ViewTransitionComponent: 30, // Experimental
|
||||
};
|
||||
} else if (gte(version, '17.0.0-alpha')) {
|
||||
ReactTypeOfWork = {
|
||||
@@ -418,6 +420,7 @@ export function getInternalReactConstants(version: string): {
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else if (gte(version, '16.6.0-beta.0')) {
|
||||
ReactTypeOfWork = {
|
||||
@@ -453,6 +456,7 @@ export function getInternalReactConstants(version: string): {
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else if (gte(version, '16.4.3-alpha')) {
|
||||
ReactTypeOfWork = {
|
||||
@@ -488,6 +492,7 @@ export function getInternalReactConstants(version: string): {
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: -1, // Removed
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
} else {
|
||||
ReactTypeOfWork = {
|
||||
@@ -523,6 +528,7 @@ export function getInternalReactConstants(version: string): {
|
||||
TracingMarkerComponent: -1, // Doesn't exist yet
|
||||
YieldComponent: 9,
|
||||
Throw: -1, // Doesn't exist yet
|
||||
ViewTransitionComponent: -1, // Doesn't exist yet
|
||||
};
|
||||
}
|
||||
// **********************************************************
|
||||
@@ -565,6 +571,7 @@ export function getInternalReactConstants(version: string): {
|
||||
SuspenseListComponent,
|
||||
TracingMarkerComponent,
|
||||
Throw,
|
||||
ViewTransitionComponent,
|
||||
} = ReactTypeOfWork;
|
||||
|
||||
function resolveFiberType(type: any): $FlowFixMe {
|
||||
@@ -673,6 +680,8 @@ export function getInternalReactConstants(version: string): {
|
||||
return 'Profiler';
|
||||
case TracingMarkerComponent:
|
||||
return 'TracingMarker';
|
||||
case ViewTransitionComponent:
|
||||
return 'ViewTransition';
|
||||
case Throw:
|
||||
// This should really never be visible.
|
||||
return 'Error';
|
||||
@@ -907,6 +916,7 @@ export function attach(
|
||||
SuspenseListComponent,
|
||||
TracingMarkerComponent,
|
||||
Throw,
|
||||
ViewTransitionComponent,
|
||||
} = ReactTypeOfWork;
|
||||
const {
|
||||
ImmediatePriority,
|
||||
@@ -1583,6 +1593,8 @@ export function attach(
|
||||
return ElementTypeSuspenseList;
|
||||
case TracingMarkerComponent:
|
||||
return ElementTypeTracingMarker;
|
||||
case ViewTransitionComponent:
|
||||
return ElementTypeViewTransition;
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export type WorkTagMap = {
|
||||
TracingMarkerComponent: WorkTag,
|
||||
YieldComponent: WorkTag,
|
||||
Throw: WorkTag,
|
||||
ViewTransitionComponent: WorkTag,
|
||||
};
|
||||
|
||||
export type HostInstance = Object;
|
||||
|
||||
@@ -50,20 +50,11 @@ const nodeToData: Map<HostInstance, Data> = new Map();
|
||||
let agent: Agent = ((null: any): Agent);
|
||||
let drawAnimationFrameID: AnimationFrameID | null = null;
|
||||
let isEnabled: boolean = false;
|
||||
let showNames: boolean = false;
|
||||
let redrawTimeoutID: TimeoutID | null = null;
|
||||
|
||||
export function initialize(injectedAgent: Agent): void {
|
||||
agent = injectedAgent;
|
||||
agent.addListener('traceUpdates', traceUpdates);
|
||||
agent.addListener('showNamesWhenTracing', (shouldShowNames: boolean) => {
|
||||
showNames = shouldShowNames;
|
||||
if (isEnabled) {
|
||||
if (drawAnimationFrameID === null) {
|
||||
drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleEnabled(value: boolean): void {
|
||||
@@ -101,9 +92,7 @@ function traceUpdates(nodes: Set<HostInstance>): void {
|
||||
rect = measureNode(node);
|
||||
}
|
||||
|
||||
let displayName = showNames
|
||||
? agent.getComponentNameForHostInstance(node)
|
||||
: null;
|
||||
let displayName = agent.getComponentNameForHostInstance(node);
|
||||
if (displayName) {
|
||||
const {baseComponentName, hocNames} = extractHOCNames(displayName);
|
||||
|
||||
@@ -127,7 +116,7 @@ function traceUpdates(nodes: Set<HostInstance>): void {
|
||||
: now + DISPLAY_DURATION,
|
||||
lastMeasuredAt,
|
||||
rect,
|
||||
displayName: showNames ? displayName : null,
|
||||
displayName,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -217,6 +217,8 @@ type FrontendEvents = {
|
||||
clearWarningsForElementID: [ElementAndRendererID],
|
||||
copyElementPath: [CopyElementPathParams],
|
||||
deletePath: [DeletePath],
|
||||
extensionComponentsPanelShown: [],
|
||||
extensionComponentsPanelHidden: [],
|
||||
getBackendVersion: [],
|
||||
getBridgeProtocol: [],
|
||||
getIfHasUnsupportedRendererVersion: [],
|
||||
@@ -234,7 +236,6 @@ type FrontendEvents = {
|
||||
renamePath: [RenamePath],
|
||||
savedPreferences: [SavedPreferencesParams],
|
||||
setTraceUpdatesEnabled: [boolean],
|
||||
setShowNamesWhenTracing: [boolean],
|
||||
shutdown: [],
|
||||
startInspectingHost: [],
|
||||
startProfiling: [StartProfilingParams],
|
||||
|
||||
@@ -50,8 +50,6 @@ export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
|
||||
'React::DevTools::traceUpdatesEnabled';
|
||||
export const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
|
||||
'React::DevTools::supportsProfiling';
|
||||
export const LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY =
|
||||
'React::DevTools::showNamesWhenTracing';
|
||||
|
||||
export const PROFILER_EXPORT_VERSION = 5;
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{
|
||||
componentFilters: [],
|
||||
error: [Error],
|
||||
hookSettings: [$ReadOnly<DevToolsHookSettings>],
|
||||
hostInstanceSelected: [Element['id']],
|
||||
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
|
||||
mutated: [[Array<number>, Map<number, number>]],
|
||||
recordChangeDescriptions: [],
|
||||
@@ -190,6 +191,9 @@ export default class Store extends EventEmitter<{
|
||||
_hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;
|
||||
_shouldShowWarningsAndErrors: boolean = false;
|
||||
|
||||
// Only used in browser extension for synchronization with built-in Elements panel.
|
||||
_lastSelectedHostInstanceElementId: Element['id'] | null = null;
|
||||
|
||||
constructor(bridge: FrontendBridge, config?: Config) {
|
||||
super();
|
||||
|
||||
@@ -265,6 +269,7 @@ export default class Store extends EventEmitter<{
|
||||
bridge.addListener('saveToClipboard', this.onSaveToClipboard);
|
||||
bridge.addListener('hookSettings', this.onHookSettings);
|
||||
bridge.addListener('backendInitialized', this.onBackendInitialized);
|
||||
bridge.addListener('selectElement', this.onHostInstanceSelected);
|
||||
}
|
||||
|
||||
// This is only used in tests to avoid memory leaks.
|
||||
@@ -481,6 +486,10 @@ export default class Store extends EventEmitter<{
|
||||
return this._unsupportedRendererVersionDetected;
|
||||
}
|
||||
|
||||
get lastSelectedHostInstanceElementId(): Element['id'] | null {
|
||||
return this._lastSelectedHostInstanceElementId;
|
||||
}
|
||||
|
||||
containsElement(id: number): boolean {
|
||||
return this._idToElement.has(id);
|
||||
}
|
||||
@@ -1431,6 +1440,7 @@ export default class Store extends EventEmitter<{
|
||||
bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
|
||||
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
|
||||
bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
|
||||
bridge.removeListener('selectElement', this.onHostInstanceSelected);
|
||||
|
||||
if (this._onBridgeProtocolTimeoutID !== null) {
|
||||
clearTimeout(this._onBridgeProtocolTimeoutID);
|
||||
@@ -1507,6 +1517,16 @@ export default class Store extends EventEmitter<{
|
||||
this._bridge.send('getHookSettings'); // Warm up cached hook settings
|
||||
};
|
||||
|
||||
onHostInstanceSelected: (elementId: number) => void = elementId => {
|
||||
if (this._lastSelectedHostInstanceElementId === elementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastSelectedHostInstanceElementId = elementId;
|
||||
// By the time we emit this, there is no guarantee that TreeContext is rendered.
|
||||
this.emit('hostInstanceSelected', elementId);
|
||||
};
|
||||
|
||||
getHookSettings: () => void = () => {
|
||||
if (this._hookSettings != null) {
|
||||
this.emit('hookSettings', this._hookSettings);
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export function printStore(
|
||||
if (state === null) {
|
||||
return '';
|
||||
}
|
||||
return state.selectedElementIndex === index ? `→` : ' ';
|
||||
return state.inspectedElementIndex === index ? `→` : ' ';
|
||||
}
|
||||
|
||||
function printErrorsAndWarnings(element: Element): string {
|
||||
|
||||
@@ -33,7 +33,7 @@ type Props = {
|
||||
|
||||
export default function Element({data, index, style}: Props): React.Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {ownerFlatTree, ownerID, selectedElementID} =
|
||||
const {ownerFlatTree, ownerID, inspectedElementID} =
|
||||
useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
|
||||
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
|
||||
const id = element === null ? null : element.id;
|
||||
const isSelected = selectedElementID === id;
|
||||
const isSelected = inspectedElementID === id;
|
||||
|
||||
const errorsAndWarningsSubscription = useMemo(
|
||||
() => ({
|
||||
|
||||
Vendored
+7
-3
@@ -28,7 +28,7 @@ function InspectedElementSourcePanel({
|
||||
symbolicatedSourcePromise,
|
||||
}: Props): React.Node {
|
||||
return (
|
||||
<div data-testname="InspectedElementView-Source">
|
||||
<div>
|
||||
<div className={styles.SourceHeaderRow}>
|
||||
<div className={styles.SourceHeader}>source</div>
|
||||
|
||||
@@ -84,7 +84,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
|
||||
const {sourceURL, line} = source;
|
||||
|
||||
return (
|
||||
<div className={styles.SourceOneLiner}>
|
||||
<div
|
||||
className={styles.SourceOneLiner}
|
||||
data-testname="InspectedElementView-FormattedSourceString">
|
||||
{formatSourceForDisplay(sourceURL, line)}
|
||||
</div>
|
||||
);
|
||||
@@ -93,7 +95,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
|
||||
const {sourceURL, line} = symbolicatedSource;
|
||||
|
||||
return (
|
||||
<div className={styles.SourceOneLiner}>
|
||||
<div
|
||||
className={styles.SourceOneLiner}
|
||||
data-testname="InspectedElementView-FormattedSourceString">
|
||||
{formatSourceForDisplay(sourceURL, line)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+9
-9
@@ -100,10 +100,10 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
[store],
|
||||
);
|
||||
|
||||
// It's very important that this context consumes selectedElementID and not NativeStyleID.
|
||||
// It's very important that this context consumes inspectedElementID and not NativeStyleID.
|
||||
// Otherwise the effect that sends the "inspect" message across the bridge-
|
||||
// would itself be blocked by the same render that suspends (waiting for the data).
|
||||
const {selectedElementID} = useContext<StateContext>(TreeStateContext);
|
||||
const {inspectedElementID} = useContext<StateContext>(TreeStateContext);
|
||||
|
||||
const [currentStyleAndLayout, setCurrentStyleAndLayout] =
|
||||
useState<StyleAndLayoutFrontend | null>(null);
|
||||
@@ -128,7 +128,7 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
resource.write(element, styleAndLayout);
|
||||
|
||||
// Schedule update with React if the currently-selected element has been invalidated.
|
||||
if (id === selectedElementID) {
|
||||
if (id === inspectedElementID) {
|
||||
setCurrentStyleAndLayout(styleAndLayout);
|
||||
}
|
||||
}
|
||||
@@ -141,15 +141,15 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
'NativeStyleEditor_styleAndLayout',
|
||||
onStyleAndLayout,
|
||||
);
|
||||
}, [bridge, currentStyleAndLayout, selectedElementID, store]);
|
||||
}, [bridge, currentStyleAndLayout, inspectedElementID, store]);
|
||||
|
||||
// This effect handler polls for updates on the currently selected element.
|
||||
useEffect(() => {
|
||||
if (selectedElementID === null) {
|
||||
if (inspectedElementID === null) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
const rendererID = store.getRendererIDForElement(inspectedElementID);
|
||||
|
||||
let timeoutID: TimeoutID | null = null;
|
||||
|
||||
@@ -158,7 +158,7 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
|
||||
if (rendererID !== null) {
|
||||
bridge.send('NativeStyleEditor_measure', {
|
||||
id: selectedElementID,
|
||||
id: inspectedElementID,
|
||||
rendererID,
|
||||
});
|
||||
}
|
||||
@@ -170,7 +170,7 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
|
||||
const onStyleAndLayout = ({id}: StyleAndLayoutBackend) => {
|
||||
// If this is the element we requested, wait a little bit and then ask for another update.
|
||||
if (id === selectedElementID) {
|
||||
if (id === inspectedElementID) {
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
@@ -190,7 +190,7 @@ function NativeStyleContextController({children}: Props): React.Node {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
}, [bridge, selectedElementID, store]);
|
||||
}, [bridge, inspectedElementID, store]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({getStyleAndLayout}),
|
||||
|
||||
+5
-5
@@ -28,19 +28,19 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
const store = useContext(StoreContext);
|
||||
const treeFocused = useContext(TreeFocusedContext);
|
||||
const {ownerID, selectedElementID} = useContext(TreeStateContext);
|
||||
const {ownerID, inspectedElementID} = useContext(TreeStateContext);
|
||||
|
||||
const subscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => {
|
||||
if (
|
||||
selectedElementID === null ||
|
||||
store.isInsideCollapsedSubTree(selectedElementID)
|
||||
inspectedElementID === null ||
|
||||
store.isInsideCollapsedSubTree(inspectedElementID)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const element = store.getElementByID(selectedElementID);
|
||||
const element = store.getElementByID(inspectedElementID);
|
||||
if (
|
||||
element === null ||
|
||||
element.isCollapsed ||
|
||||
@@ -83,7 +83,7 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
|
||||
};
|
||||
},
|
||||
}),
|
||||
[selectedElementID, store],
|
||||
[inspectedElementID, store],
|
||||
);
|
||||
const data = useSubscription<Data | null>(subscription);
|
||||
|
||||
|
||||
@@ -37,66 +37,72 @@ import styles from './Tree.css';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility';
|
||||
|
||||
// Never indent more than this number of pixels (even if we have the room).
|
||||
const DEFAULT_INDENTATION_SIZE = 12;
|
||||
|
||||
export type ItemData = {
|
||||
numElements: number,
|
||||
isNavigatingWithKeyboard: boolean,
|
||||
lastScrolledIDRef: {current: number | null, ...},
|
||||
onElementMouseEnter: (id: number) => void,
|
||||
treeFocused: boolean,
|
||||
};
|
||||
|
||||
type Props = {};
|
||||
function calculateInitialScrollOffset(
|
||||
inspectedElementIndex: number | null,
|
||||
elementHeight: number,
|
||||
): number | void {
|
||||
if (inspectedElementIndex === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function Tree(props: Props): React.Node {
|
||||
if (inspectedElementIndex < 3) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Make 3 elements on top of the inspected one visible
|
||||
return (inspectedElementIndex - 3) * elementHeight;
|
||||
}
|
||||
|
||||
export default function Tree(): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {
|
||||
numElements,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
} = useContext(TreeStateContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const {hideSettings} = useContext(OptionsContext);
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
|
||||
const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] =
|
||||
useState(false);
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
const [treeFocused, setTreeFocused] = useState<boolean>(false);
|
||||
const componentsPanelVisible = useExtensionComponentsPanelVisibility(bridge);
|
||||
|
||||
const treeRef = useRef<HTMLDivElement | null>(null);
|
||||
const focusTargetRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const [treeFocused, setTreeFocused] = useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
if (!componentsPanelVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
|
||||
// Make sure a newly selected element is visible in the list.
|
||||
// This is helpful for things like the owners list and search.
|
||||
//
|
||||
// TRICKY:
|
||||
// It's important to use a callback ref for this, rather than a ref object and an effect.
|
||||
// As an optimization, the AutoSizer component does not render children when their size would be 0.
|
||||
// This means that in some cases (if the browser panel size is initially really small),
|
||||
// the Tree component might render without rendering an inner List.
|
||||
// In this case, the list ref would be null on mount (when the scroll effect runs),
|
||||
// meaning the scroll action would be skipped (since ref updates don't re-run effects).
|
||||
// Using a callback ref accounts for this case...
|
||||
const listCallbackRef = useCallback(
|
||||
(list: $FlowFixMe) => {
|
||||
if (list != null && selectedElementIndex !== null) {
|
||||
list.scrollToItem(selectedElementIndex, 'smart');
|
||||
}
|
||||
},
|
||||
[selectedElementIndex],
|
||||
);
|
||||
if (listRef.current != null && inspectedElementIndex !== null) {
|
||||
listRef.current.scrollToItem(inspectedElementIndex, 'smart');
|
||||
}
|
||||
}, [inspectedElementIndex, componentsPanelVisible]);
|
||||
|
||||
// Picking an element in the inspector should put focus into the tree.
|
||||
// This ensures that keyboard navigation works right after picking a node.
|
||||
// If possible, navigation works right after picking a node.
|
||||
// NOTE: This is not guaranteed to work, because browser extension panels are hosted inside an iframe.
|
||||
useEffect(() => {
|
||||
function handleStopInspectingHost(didSelectNode: boolean) {
|
||||
if (didSelectNode && focusTargetRef.current !== null) {
|
||||
@@ -112,11 +118,6 @@ export default function Tree(props: Props): React.Node {
|
||||
bridge.removeListener('stopInspectingHost', handleStopInspectingHost);
|
||||
}, [bridge]);
|
||||
|
||||
// This ref is passed down the context to elements.
|
||||
// It lets them avoid autoscrolling to the same item many times
|
||||
// when a selected virtual row goes in and out of the viewport.
|
||||
const lastScrolledIDRef = useRef<number | null>(null);
|
||||
|
||||
// Navigate the tree with up/down arrow keys.
|
||||
useEffect(() => {
|
||||
if (treeRef.current === null) {
|
||||
@@ -141,8 +142,8 @@ export default function Tree(props: Props): React.Node {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
element =
|
||||
selectedElementID !== null
|
||||
? store.getElementByID(selectedElementID)
|
||||
inspectedElementID !== null
|
||||
? store.getElementByID(inspectedElementID)
|
||||
: null;
|
||||
if (element !== null) {
|
||||
if (event.altKey) {
|
||||
@@ -161,8 +162,8 @@ export default function Tree(props: Props): React.Node {
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
element =
|
||||
selectedElementID !== null
|
||||
? store.getElementByID(selectedElementID)
|
||||
inspectedElementID !== null
|
||||
? store.getElementByID(inspectedElementID)
|
||||
: null;
|
||||
if (element !== null) {
|
||||
if (event.altKey) {
|
||||
@@ -210,35 +211,26 @@ export default function Tree(props: Props): React.Node {
|
||||
return () => {
|
||||
container.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [dispatch, selectedElementID, store]);
|
||||
}, [dispatch, inspectedElementID, store]);
|
||||
|
||||
// Focus management.
|
||||
const handleBlur = useCallback(() => setTreeFocused(false), []);
|
||||
const handleFocus = useCallback(() => {
|
||||
setTreeFocused(true);
|
||||
|
||||
if (selectedElementIndex === null && numElements > 0) {
|
||||
dispatch({
|
||||
type: 'SELECT_ELEMENT_AT_INDEX',
|
||||
payload: 0,
|
||||
});
|
||||
}
|
||||
}, [dispatch, numElements, selectedElementIndex]);
|
||||
const handleFocus = useCallback(() => setTreeFocused(true), []);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(event: $FlowFixMe) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (selectedElementID !== null) {
|
||||
dispatch({type: 'SELECT_OWNER', payload: selectedElementID});
|
||||
if (inspectedElementID !== null) {
|
||||
dispatch({type: 'SELECT_OWNER', payload: inspectedElementID});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[dispatch, selectedElementID],
|
||||
[dispatch, inspectedElementID],
|
||||
);
|
||||
|
||||
// If we switch the selected element while using the keyboard,
|
||||
@@ -255,8 +247,8 @@ export default function Tree(props: Props): React.Node {
|
||||
didSelectNewSearchResult = true;
|
||||
}
|
||||
if (isNavigatingWithKeyboard || didSelectNewSearchResult) {
|
||||
if (selectedElementID !== null) {
|
||||
highlightHostInstance(selectedElementID);
|
||||
if (inspectedElementID !== null) {
|
||||
highlightHostInstance(inspectedElementID);
|
||||
} else {
|
||||
clearHighlightHostInstance();
|
||||
}
|
||||
@@ -267,7 +259,7 @@ export default function Tree(props: Props): React.Node {
|
||||
highlightHostInstance,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
selectedElementID,
|
||||
inspectedElementID,
|
||||
]);
|
||||
|
||||
// Highlight last hovered element.
|
||||
@@ -294,19 +286,11 @@ export default function Tree(props: Props): React.Node {
|
||||
// This includes the owner context, since it controls a filtered view of the tree.
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
numElements,
|
||||
isNavigatingWithKeyboard,
|
||||
onElementMouseEnter: handleElementMouseEnter,
|
||||
lastScrolledIDRef,
|
||||
treeFocused,
|
||||
}),
|
||||
[
|
||||
numElements,
|
||||
isNavigatingWithKeyboard,
|
||||
handleElementMouseEnter,
|
||||
lastScrolledIDRef,
|
||||
treeFocused,
|
||||
],
|
||||
[isNavigatingWithKeyboard, handleElementMouseEnter, treeFocused],
|
||||
);
|
||||
|
||||
const itemKey = useCallback(
|
||||
@@ -426,12 +410,16 @@ export default function Tree(props: Props): React.Node {
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
initialScrollOffset={calculateInitialScrollOffset(
|
||||
inspectedElementIndex,
|
||||
lineHeight,
|
||||
)}
|
||||
innerElementType={InnerElementType}
|
||||
itemCount={numElements}
|
||||
itemData={itemData}
|
||||
itemKey={itemKey}
|
||||
itemSize={lineHeight}
|
||||
ref={listCallbackRef}
|
||||
ref={listRef}
|
||||
width={width}>
|
||||
{Element}
|
||||
</FixedSizeList>
|
||||
|
||||
+153
-189
@@ -29,17 +29,15 @@ import type {ReactContext} from 'shared/ReactTypes';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
startTransition,
|
||||
} from 'react';
|
||||
import {createRegExp} from '../utils';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import {StoreContext} from '../context';
|
||||
import Store from '../../store';
|
||||
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -48,8 +46,6 @@ export type StateContext = {
|
||||
// Tree
|
||||
numElements: number,
|
||||
ownerSubtreeLeafElementID: number | null,
|
||||
selectedElementID: number | null,
|
||||
selectedElementIndex: number | null,
|
||||
|
||||
// Search
|
||||
searchIndex: number | null,
|
||||
@@ -62,6 +58,7 @@ export type StateContext = {
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
};
|
||||
|
||||
type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
|
||||
@@ -123,9 +120,6 @@ type ACTION_SET_SEARCH_TEXT = {
|
||||
type: 'SET_SEARCH_TEXT',
|
||||
payload: string,
|
||||
};
|
||||
type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {
|
||||
type: 'UPDATE_INSPECTED_ELEMENT_ID',
|
||||
};
|
||||
|
||||
type Action =
|
||||
| ACTION_GO_TO_NEXT_SEARCH_RESULT
|
||||
@@ -145,8 +139,7 @@ type Action =
|
||||
| ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
|
||||
| ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
|
||||
| ACTION_SET_SEARCH_TEXT
|
||||
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
|
||||
| ACTION_SET_SEARCH_TEXT;
|
||||
|
||||
export type DispatcherContext = (action: Action) => void;
|
||||
|
||||
@@ -162,8 +155,6 @@ type State = {
|
||||
// Tree
|
||||
numElements: number,
|
||||
ownerSubtreeLeafElementID: number | null,
|
||||
selectedElementID: number | null,
|
||||
selectedElementIndex: number | null,
|
||||
|
||||
// Search
|
||||
searchIndex: number | null,
|
||||
@@ -176,14 +167,15 @@ type State = {
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
inspectedElementIndex: number | null,
|
||||
};
|
||||
|
||||
function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
numElements,
|
||||
ownerSubtreeLeafElementID,
|
||||
selectedElementIndex,
|
||||
selectedElementID,
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
} = state;
|
||||
const ownerID = state.ownerID;
|
||||
|
||||
@@ -201,34 +193,33 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
// We deduce the parent-child mapping from removedIDs (id -> parentID)
|
||||
// because by now it's too late to read them from the store.
|
||||
while (
|
||||
selectedElementID !== null &&
|
||||
removedIDs.has(selectedElementID)
|
||||
inspectedElementID !== null &&
|
||||
removedIDs.has(inspectedElementID)
|
||||
) {
|
||||
selectedElementID = ((removedIDs.get(
|
||||
selectedElementID,
|
||||
): any): number);
|
||||
// $FlowExpectedError[incompatible-type]
|
||||
inspectedElementID = removedIDs.get(inspectedElementID);
|
||||
}
|
||||
if (selectedElementID === 0) {
|
||||
if (inspectedElementID === 0) {
|
||||
// The whole root was removed.
|
||||
selectedElementIndex = null;
|
||||
inspectedElementIndex = null;
|
||||
}
|
||||
break;
|
||||
case 'SELECT_CHILD_ELEMENT_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (selectedElementIndex !== null) {
|
||||
const selectedElement = store.getElementAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
if (inspectedElementIndex !== null) {
|
||||
const inspectedElement = store.getElementAtIndex(
|
||||
inspectedElementIndex,
|
||||
);
|
||||
if (
|
||||
selectedElement !== null &&
|
||||
selectedElement.children.length > 0 &&
|
||||
!selectedElement.isCollapsed
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.children.length > 0 &&
|
||||
!inspectedElement.isCollapsed
|
||||
) {
|
||||
const firstChildID = selectedElement.children[0];
|
||||
const firstChildID = inspectedElement.children[0];
|
||||
const firstChildIndex = store.getIndexOfElementID(firstChildID);
|
||||
if (firstChildIndex !== null) {
|
||||
selectedElementIndex = firstChildIndex;
|
||||
inspectedElementIndex = firstChildIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +227,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
|
||||
inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
|
||||
.payload;
|
||||
break;
|
||||
case 'SELECT_ELEMENT_BY_ID':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
@@ -245,30 +237,30 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
|
||||
lookupIDForIndex = false;
|
||||
|
||||
selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
|
||||
selectedElementIndex =
|
||||
selectedElementID === null
|
||||
inspectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
|
||||
inspectedElementIndex =
|
||||
inspectedElementID === null
|
||||
? null
|
||||
: store.getIndexOfElementID(selectedElementID);
|
||||
: store.getIndexOfElementID(inspectedElementID);
|
||||
break;
|
||||
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (
|
||||
selectedElementIndex === null ||
|
||||
selectedElementIndex + 1 >= numElements
|
||||
inspectedElementIndex === null ||
|
||||
inspectedElementIndex + 1 >= numElements
|
||||
) {
|
||||
selectedElementIndex = 0;
|
||||
inspectedElementIndex = 0;
|
||||
} else {
|
||||
selectedElementIndex++;
|
||||
inspectedElementIndex++;
|
||||
}
|
||||
break;
|
||||
case 'SELECT_NEXT_SIBLING_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
const selectedElement = store.getElementAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
((inspectedElementIndex: any): number),
|
||||
);
|
||||
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
||||
const parent = store.getElementByID(selectedElement.parentID);
|
||||
@@ -279,23 +271,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
selectedChildIndex < children.length - 1
|
||||
? children[selectedChildIndex + 1]
|
||||
: children[0];
|
||||
selectedElementIndex = store.getIndexOfElementID(nextChildID);
|
||||
inspectedElementIndex = store.getIndexOfElementID(nextChildID);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
if (
|
||||
ownerSubtreeLeafElementID !== null &&
|
||||
ownerSubtreeLeafElementID !== selectedElementID
|
||||
ownerSubtreeLeafElementID !== inspectedElementID
|
||||
) {
|
||||
const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
|
||||
if (leafElement !== null) {
|
||||
let currentElement: null | Element = leafElement;
|
||||
while (currentElement !== null) {
|
||||
if (currentElement.ownerID === selectedElementID) {
|
||||
selectedElementIndex = store.getIndexOfElementID(
|
||||
if (currentElement.ownerID === inspectedElementID) {
|
||||
inspectedElementIndex = store.getIndexOfElementID(
|
||||
currentElement.id,
|
||||
);
|
||||
break;
|
||||
@@ -308,23 +300,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
break;
|
||||
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
if (ownerSubtreeLeafElementID === null) {
|
||||
// If this is the first time we're stepping through the owners tree,
|
||||
// pin the current component as the owners list leaf.
|
||||
// This will enable us to step back down to this component.
|
||||
ownerSubtreeLeafElementID = selectedElementID;
|
||||
ownerSubtreeLeafElementID = inspectedElementID;
|
||||
}
|
||||
|
||||
const selectedElement = store.getElementAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
((inspectedElementIndex: any): number),
|
||||
);
|
||||
if (selectedElement !== null && selectedElement.ownerID !== 0) {
|
||||
const ownerIndex = store.getIndexOfElementID(
|
||||
selectedElement.ownerID,
|
||||
);
|
||||
if (ownerIndex !== null) {
|
||||
selectedElementIndex = ownerIndex;
|
||||
inspectedElementIndex = ownerIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,16 +324,16 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
case 'SELECT_PARENT_ELEMENT_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
const selectedElement = store.getElementAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
((inspectedElementIndex: any): number),
|
||||
);
|
||||
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
||||
const parentIndex = store.getIndexOfElementID(
|
||||
selectedElement.parentID,
|
||||
);
|
||||
if (parentIndex !== null) {
|
||||
selectedElementIndex = parentIndex;
|
||||
inspectedElementIndex = parentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,18 +341,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (selectedElementIndex === null || selectedElementIndex === 0) {
|
||||
selectedElementIndex = numElements - 1;
|
||||
if (inspectedElementIndex === null || inspectedElementIndex === 0) {
|
||||
inspectedElementIndex = numElements - 1;
|
||||
} else {
|
||||
selectedElementIndex--;
|
||||
inspectedElementIndex--;
|
||||
}
|
||||
break;
|
||||
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
|
||||
ownerSubtreeLeafElementID = null;
|
||||
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
const selectedElement = store.getElementAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
((inspectedElementIndex: any): number),
|
||||
);
|
||||
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
||||
const parent = store.getElementByID(selectedElement.parentID);
|
||||
@@ -371,7 +363,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
selectedChildIndex > 0
|
||||
? children[selectedChildIndex - 1]
|
||||
: children[children.length - 1];
|
||||
selectedElementIndex = store.getIndexOfElementID(nextChildID);
|
||||
inspectedElementIndex = store.getIndexOfElementID(nextChildID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +376,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
|
||||
let flatIndex = 0;
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
// Resume from the current position in the list.
|
||||
// Otherwise step to the previous item, relative to the current selection.
|
||||
for (
|
||||
@@ -393,7 +385,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
i--
|
||||
) {
|
||||
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
||||
if (index >= selectedElementIndex) {
|
||||
if (index >= inspectedElementIndex) {
|
||||
flatIndex = i;
|
||||
} else {
|
||||
break;
|
||||
@@ -407,12 +399,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
elementIndicesWithErrorsOrWarnings[
|
||||
elementIndicesWithErrorsOrWarnings.length - 1
|
||||
];
|
||||
selectedElementID = prevEntry.id;
|
||||
selectedElementIndex = prevEntry.index;
|
||||
inspectedElementID = prevEntry.id;
|
||||
inspectedElementIndex = prevEntry.index;
|
||||
} else {
|
||||
prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
|
||||
selectedElementID = prevEntry.id;
|
||||
selectedElementIndex = prevEntry.index;
|
||||
inspectedElementID = prevEntry.id;
|
||||
inspectedElementIndex = prevEntry.index;
|
||||
}
|
||||
|
||||
lookupIDForIndex = false;
|
||||
@@ -426,12 +418,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
|
||||
let flatIndex = -1;
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
// Resume from the current position in the list.
|
||||
// Otherwise step to the next item, relative to the current selection.
|
||||
for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
|
||||
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
||||
if (index <= selectedElementIndex) {
|
||||
if (index <= inspectedElementIndex) {
|
||||
flatIndex = i;
|
||||
} else {
|
||||
break;
|
||||
@@ -442,12 +434,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
let nextEntry;
|
||||
if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
|
||||
nextEntry = elementIndicesWithErrorsOrWarnings[0];
|
||||
selectedElementID = nextEntry.id;
|
||||
selectedElementIndex = nextEntry.index;
|
||||
inspectedElementID = nextEntry.id;
|
||||
inspectedElementIndex = nextEntry.index;
|
||||
} else {
|
||||
nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
|
||||
selectedElementID = nextEntry.id;
|
||||
selectedElementIndex = nextEntry.index;
|
||||
inspectedElementID = nextEntry.id;
|
||||
inspectedElementIndex = nextEntry.index;
|
||||
}
|
||||
|
||||
lookupIDForIndex = false;
|
||||
@@ -460,12 +452,15 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
|
||||
// Keep selected item ID and index in sync.
|
||||
if (lookupIDForIndex && selectedElementIndex !== state.selectedElementIndex) {
|
||||
if (selectedElementIndex === null) {
|
||||
selectedElementID = null;
|
||||
if (
|
||||
lookupIDForIndex &&
|
||||
inspectedElementIndex !== state.inspectedElementIndex
|
||||
) {
|
||||
if (inspectedElementIndex === null) {
|
||||
inspectedElementID = null;
|
||||
} else {
|
||||
selectedElementID = store.getElementIDAtIndex(
|
||||
((selectedElementIndex: any): number),
|
||||
inspectedElementID = store.getElementIDAtIndex(
|
||||
((inspectedElementIndex: any): number),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -475,8 +470,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
|
||||
numElements,
|
||||
ownerSubtreeLeafElementID,
|
||||
selectedElementIndex,
|
||||
selectedElementID,
|
||||
inspectedElementIndex,
|
||||
inspectedElementID,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -485,8 +480,8 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
} = state;
|
||||
const ownerID = state.ownerID;
|
||||
|
||||
@@ -594,11 +589,11 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
});
|
||||
if (searchResults.length > 0) {
|
||||
if (prevSearchIndex === null) {
|
||||
if (selectedElementIndex !== null) {
|
||||
if (inspectedElementIndex !== null) {
|
||||
searchIndex = getNearestResultIndex(
|
||||
store,
|
||||
searchResults,
|
||||
selectedElementIndex,
|
||||
inspectedElementIndex,
|
||||
);
|
||||
} else {
|
||||
searchIndex = 0;
|
||||
@@ -619,7 +614,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
|
||||
if (searchText !== prevSearchText) {
|
||||
const newSearchIndex = searchResults.indexOf(selectedElementID);
|
||||
const newSearchIndex = searchResults.indexOf(inspectedElementID);
|
||||
if (newSearchIndex === -1) {
|
||||
// Only move the selection if the new query
|
||||
// doesn't match the current selection anymore.
|
||||
@@ -631,17 +626,17 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
}
|
||||
if (didRequestSearch && searchIndex !== null) {
|
||||
selectedElementID = ((searchResults[searchIndex]: any): number);
|
||||
selectedElementIndex = store.getIndexOfElementID(
|
||||
((selectedElementID: any): number),
|
||||
inspectedElementID = ((searchResults[searchIndex]: any): number);
|
||||
inspectedElementIndex = store.getIndexOfElementID(
|
||||
((inspectedElementID: any): number),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
|
||||
searchIndex,
|
||||
searchResults,
|
||||
@@ -652,14 +647,14 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
numElements,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
} = state;
|
||||
const {searchIndex, searchResults, searchText} = state;
|
||||
|
||||
let prevSelectedElementIndex = selectedElementIndex;
|
||||
let prevInspectedElementIndex = inspectedElementIndex;
|
||||
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
@@ -667,75 +662,76 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
if (!store.containsElement(ownerID)) {
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementID = null;
|
||||
prevInspectedElementIndex = null;
|
||||
} else {
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
if (selectedElementID !== null) {
|
||||
if (inspectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === selectedElementID,
|
||||
prevInspectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === inspectedElementID,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selectedElementID !== null) {
|
||||
if (inspectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = store.getIndexOfElementID(selectedElementID);
|
||||
inspectedElementIndex = store.getIndexOfElementID(inspectedElementID);
|
||||
}
|
||||
}
|
||||
if (selectedElementIndex === -1) {
|
||||
if (inspectedElementIndex === -1) {
|
||||
// If we couldn't find this ID after mutation, unselect it.
|
||||
selectedElementIndex = null;
|
||||
selectedElementID = null;
|
||||
inspectedElementIndex = null;
|
||||
inspectedElementID = null;
|
||||
}
|
||||
break;
|
||||
case 'RESET_OWNER_STACK':
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementIndex =
|
||||
selectedElementID !== null
|
||||
? store.getIndexOfElementID(selectedElementID)
|
||||
inspectedElementIndex =
|
||||
inspectedElementID !== null
|
||||
? store.getIndexOfElementID(inspectedElementID)
|
||||
: null;
|
||||
break;
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
if (ownerFlatTree !== null) {
|
||||
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
|
||||
inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
|
||||
.payload;
|
||||
}
|
||||
break;
|
||||
case 'SELECT_ELEMENT_BY_ID':
|
||||
if (ownerFlatTree !== null) {
|
||||
const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
|
||||
if (payload === null) {
|
||||
selectedElementIndex = null;
|
||||
inspectedElementIndex = null;
|
||||
} else {
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
inspectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === payload,
|
||||
);
|
||||
|
||||
// If the selected element is outside of the current owners list,
|
||||
// exit the list and select the element in the main tree.
|
||||
// This supports features like toggling Suspense.
|
||||
if (selectedElementIndex !== null && selectedElementIndex < 0) {
|
||||
if (inspectedElementIndex !== null && inspectedElementIndex < 0) {
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementIndex = store.getIndexOfElementID(payload);
|
||||
inspectedElementIndex = store.getIndexOfElementID(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
||||
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
|
||||
if (selectedElementIndex === null) {
|
||||
selectedElementIndex = 0;
|
||||
} else if (selectedElementIndex + 1 < ownerFlatTree.length) {
|
||||
selectedElementIndex++;
|
||||
if (inspectedElementIndex === null) {
|
||||
inspectedElementIndex = 0;
|
||||
} else if (inspectedElementIndex + 1 < ownerFlatTree.length) {
|
||||
inspectedElementIndex++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
||||
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
|
||||
if (selectedElementIndex !== null && selectedElementIndex > 0) {
|
||||
selectedElementIndex--;
|
||||
if (inspectedElementIndex !== null && inspectedElementIndex > 0) {
|
||||
inspectedElementIndex--;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -747,8 +743,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
|
||||
// Always force reset selection to be the top of the new owner tree.
|
||||
selectedElementIndex = 0;
|
||||
prevSelectedElementIndex = null;
|
||||
inspectedElementIndex = 0;
|
||||
prevInspectedElementIndex = null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -769,12 +765,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
|
||||
// Keep selected item ID and index in sync.
|
||||
if (selectedElementIndex !== prevSelectedElementIndex) {
|
||||
if (selectedElementIndex === null) {
|
||||
selectedElementID = null;
|
||||
if (inspectedElementIndex !== prevInspectedElementIndex) {
|
||||
if (inspectedElementIndex === null) {
|
||||
inspectedElementID = null;
|
||||
} else {
|
||||
if (ownerFlatTree !== null) {
|
||||
selectedElementID = ownerFlatTree[selectedElementIndex].id;
|
||||
inspectedElementID = ownerFlatTree[inspectedElementIndex].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,8 +779,6 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
...state,
|
||||
|
||||
numElements,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
|
||||
searchIndex,
|
||||
searchResults,
|
||||
@@ -792,51 +786,28 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
|
||||
inspectedElementID,
|
||||
inspectedElementIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function reduceSuspenseState(
|
||||
store: Store,
|
||||
state: State,
|
||||
action: Action,
|
||||
): State {
|
||||
const {type} = action;
|
||||
switch (type) {
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
if (state.inspectedElementID !== state.selectedElementID) {
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
|
||||
// Used for automated testing
|
||||
defaultInspectedElementID?: ?number,
|
||||
defaultOwnerID?: ?number,
|
||||
defaultSelectedElementID?: ?number,
|
||||
defaultSelectedElementIndex?: ?number,
|
||||
defaultInspectedElementID?: ?number,
|
||||
defaultInspectedElementIndex?: ?number,
|
||||
};
|
||||
|
||||
// TODO Remove TreeContextController wrapper element once global Context.write API exists.
|
||||
function TreeContextController({
|
||||
children,
|
||||
defaultInspectedElementID,
|
||||
defaultOwnerID,
|
||||
defaultSelectedElementID,
|
||||
defaultSelectedElementIndex,
|
||||
defaultInspectedElementID,
|
||||
defaultInspectedElementIndex,
|
||||
}: Props): React.Node {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const initialRevision = useMemo(() => store.revision, [store]);
|
||||
@@ -866,23 +837,22 @@ function TreeContextController({
|
||||
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
|
||||
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
|
||||
case 'SELECT_OWNER':
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
case 'SET_SEARCH_TEXT':
|
||||
state = reduceTreeState(store, state, action);
|
||||
state = reduceSearchState(store, state, action);
|
||||
state = reduceOwnersState(store, state, action);
|
||||
state = reduceSuspenseState(store, state, action);
|
||||
|
||||
// TODO(hoxyq): review
|
||||
// If the selected ID is in a collapsed subtree, reset the selected index to null.
|
||||
// We'll know the correct index after the layout effect will toggle the tree,
|
||||
// and the store tree is mutated to account for that.
|
||||
if (
|
||||
state.selectedElementID !== null &&
|
||||
store.isInsideCollapsedSubTree(state.selectedElementID)
|
||||
state.inspectedElementID !== null &&
|
||||
store.isInsideCollapsedSubTree(state.inspectedElementID)
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
selectedElementIndex: null,
|
||||
inspectedElementIndex: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -898,10 +868,6 @@ function TreeContextController({
|
||||
// Tree
|
||||
numElements: store.numElements,
|
||||
ownerSubtreeLeafElementID: null,
|
||||
selectedElementID:
|
||||
defaultSelectedElementID == null ? null : defaultSelectedElementID,
|
||||
selectedElementIndex:
|
||||
defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex,
|
||||
|
||||
// Search
|
||||
searchIndex: null,
|
||||
@@ -914,42 +880,41 @@ function TreeContextController({
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID:
|
||||
defaultInspectedElementID == null ? null : defaultInspectedElementID,
|
||||
defaultInspectedElementID != null
|
||||
? defaultInspectedElementID
|
||||
: store.lastSelectedHostInstanceElementId,
|
||||
inspectedElementIndex:
|
||||
defaultInspectedElementIndex != null
|
||||
? defaultInspectedElementIndex
|
||||
: store.lastSelectedHostInstanceElementId
|
||||
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
|
||||
: null,
|
||||
});
|
||||
|
||||
const dispatchWrapper = useCallback(
|
||||
(action: Action) => {
|
||||
dispatch(action);
|
||||
startTransition(() => {
|
||||
dispatch({type: 'UPDATE_INSPECTED_ELEMENT_ID'});
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Listen for host element selections.
|
||||
useEffect(() => {
|
||||
const handleSelectElement = (id: number) =>
|
||||
dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
||||
bridge.addListener('selectElement', handleSelectElement);
|
||||
return () => bridge.removeListener('selectElement', handleSelectElement);
|
||||
}, [bridge, dispatchWrapper]);
|
||||
const handler = (id: Element['id']) =>
|
||||
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
||||
|
||||
store.addListener('hostInstanceSelected', handler);
|
||||
return () => store.removeListener('hostInstanceSelected', handler);
|
||||
}, [store, dispatch]);
|
||||
|
||||
// If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
|
||||
// This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
|
||||
const prevSelectedElementID = useRef<number | null>(null);
|
||||
const prevInspectedElementID = useRef<number | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (state.selectedElementID !== prevSelectedElementID.current) {
|
||||
prevSelectedElementID.current = state.selectedElementID;
|
||||
if (state.inspectedElementID !== prevInspectedElementID.current) {
|
||||
prevInspectedElementID.current = state.inspectedElementID;
|
||||
|
||||
if (state.selectedElementID !== null) {
|
||||
const element = store.getElementByID(state.selectedElementID);
|
||||
if (state.inspectedElementID !== null) {
|
||||
const element = store.getElementByID(state.inspectedElementID);
|
||||
if (element !== null && element.parentID > 0) {
|
||||
store.toggleIsCollapsed(element.parentID, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [state.selectedElementID, store]);
|
||||
}, [state.inspectedElementID, store]);
|
||||
|
||||
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
|
||||
useEffect(() => {
|
||||
@@ -957,7 +922,7 @@ function TreeContextController({
|
||||
Array<number>,
|
||||
Map<number, number>,
|
||||
]) => {
|
||||
dispatchWrapper({
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [addedElementIDs, removedElementIDs],
|
||||
});
|
||||
@@ -968,20 +933,19 @@ function TreeContextController({
|
||||
// At the moment, we can treat this as a mutation.
|
||||
// We don't know which Elements were newly added/removed, but that should be okay in this case.
|
||||
// It would only impact the search state, which is unlikely to exist yet at this point.
|
||||
dispatchWrapper({
|
||||
dispatch({
|
||||
type: 'HANDLE_STORE_MUTATION',
|
||||
payload: [[], new Map()],
|
||||
});
|
||||
}
|
||||
|
||||
store.addListener('mutated', handleStoreMutated);
|
||||
|
||||
return () => store.removeListener('mutated', handleStoreMutated);
|
||||
}, [dispatchWrapper, initialRevision, store]);
|
||||
}, [dispatch, initialRevision, store]);
|
||||
|
||||
return (
|
||||
<TreeStateContext.Provider value={state}>
|
||||
<TreeDispatcherContext.Provider value={dispatchWrapper}>
|
||||
<TreeDispatcherContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</TreeDispatcherContext.Provider>
|
||||
</TreeStateContext.Provider>
|
||||
@@ -1020,11 +984,11 @@ function recursivelySearchTree(
|
||||
function getNearestResultIndex(
|
||||
store: Store,
|
||||
searchResults: Array<number>,
|
||||
selectedElementIndex: number,
|
||||
inspectedElementIndex: number,
|
||||
): number {
|
||||
const index = searchResults.findIndex(id => {
|
||||
const innerIndex = store.getIndexOfElementID(id);
|
||||
return innerIndex !== null && innerIndex >= selectedElementIndex;
|
||||
return innerIndex !== null && innerIndex >= inspectedElementIndex;
|
||||
});
|
||||
|
||||
return index === -1 ? 0 : index;
|
||||
|
||||
+3
-3
@@ -88,7 +88,7 @@ type Props = {
|
||||
|
||||
function ProfilerContextController({children}: Props): React.Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {selectedElementID} = useContext(TreeStateContext);
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const {profilerStore} = store;
|
||||
@@ -176,9 +176,9 @@ function ProfilerContextController({children}: Props): React.Node {
|
||||
|
||||
if (rootID === null || !dataForRoots.has(rootID)) {
|
||||
let selectedElementRootID = null;
|
||||
if (selectedElementID !== null) {
|
||||
if (inspectedElementID !== null) {
|
||||
selectedElementRootID =
|
||||
store.getRootIDForElement(selectedElementID);
|
||||
store.getRootIDForElement(inspectedElementID);
|
||||
}
|
||||
if (
|
||||
selectedElementRootID !== null &&
|
||||
|
||||
-15
@@ -34,10 +34,8 @@ export default function GeneralSettings(_: {}): React.Node {
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
setTraceUpdatesEnabled,
|
||||
setShowNamesWhenTracing,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
showNamesWhenTracing,
|
||||
} = useContext(SettingsContext);
|
||||
|
||||
const {backendVersion, supportsTraceUpdates} = useContext(StoreContext);
|
||||
@@ -85,19 +83,6 @@ export default function GeneralSettings(_: {}): React.Node {
|
||||
/>{' '}
|
||||
Highlight updates when components render.
|
||||
</label>
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showNamesWhenTracing}
|
||||
disabled={!traceUpdatesEnabled}
|
||||
onChange={({currentTarget}) =>
|
||||
setShowNamesWhenTracing(currentTarget.checked)
|
||||
}
|
||||
/>{' '}
|
||||
Show component names while highlighting.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
-16
@@ -21,7 +21,6 @@ import {
|
||||
LOCAL_STORAGE_BROWSER_THEME,
|
||||
LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {
|
||||
COMFORTABLE_LINE_HEIGHT,
|
||||
@@ -54,9 +53,6 @@ type Context = {
|
||||
|
||||
traceUpdatesEnabled: boolean,
|
||||
setTraceUpdatesEnabled: (value: boolean) => void,
|
||||
|
||||
showNamesWhenTracing: boolean,
|
||||
setShowNamesWhenTracing: (showNames: boolean) => void,
|
||||
};
|
||||
|
||||
const SettingsContext: ReactContext<Context> = createContext<Context>(
|
||||
@@ -115,11 +111,6 @@ function SettingsContextController({
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
false,
|
||||
);
|
||||
const [showNamesWhenTracing, setShowNamesWhenTracing] =
|
||||
useLocalStorageWithLog<boolean>(
|
||||
LOCAL_STORAGE_SHOW_NAMES_WHEN_TRACING_KEY,
|
||||
true,
|
||||
);
|
||||
|
||||
const documentElements = useMemo<DocumentElements>(() => {
|
||||
const array: Array<HTMLElement> = [
|
||||
@@ -173,10 +164,6 @@ function SettingsContextController({
|
||||
bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
|
||||
}, [bridge, traceUpdatesEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
bridge.send('setShowNamesWhenTracing', showNamesWhenTracing);
|
||||
}, [bridge, showNamesWhenTracing]);
|
||||
|
||||
const value: Context = useMemo(
|
||||
() => ({
|
||||
displayDensity,
|
||||
@@ -192,8 +179,6 @@ function SettingsContextController({
|
||||
theme,
|
||||
browserTheme,
|
||||
traceUpdatesEnabled,
|
||||
showNamesWhenTracing,
|
||||
setShowNamesWhenTracing,
|
||||
}),
|
||||
[
|
||||
displayDensity,
|
||||
@@ -205,7 +190,6 @@ function SettingsContextController({
|
||||
theme,
|
||||
browserTheme,
|
||||
traceUpdatesEnabled,
|
||||
showNamesWhenTracing,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 {useState, useEffect} from 'react';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
// Events that are prefixed with `extension` will only be emitted for the browser extension implementation.
|
||||
// For other implementations, this hook will just return constant `true` value.
|
||||
export function useExtensionComponentsPanelVisibility(
|
||||
bridge: FrontendBridge,
|
||||
): boolean {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
function onPanelShown() {
|
||||
setIsVisible(true);
|
||||
}
|
||||
function onPanelHidden() {
|
||||
setIsVisible(false);
|
||||
}
|
||||
|
||||
bridge.addListener('extensionComponentsPanelShown', onPanelShown);
|
||||
bridge.addListener('extensionComponentsPanelHidden', onPanelHidden);
|
||||
|
||||
return () => {
|
||||
bridge.removeListener('extensionComponentsPanelShown', onPanelShown);
|
||||
bridge.removeListener('extensionComponentsPanelHidden', onPanelHidden);
|
||||
};
|
||||
}, [bridge]);
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
+3
-1
@@ -49,6 +49,7 @@ export const ElementTypeSuspense = 12;
|
||||
export const ElementTypeSuspenseList = 13;
|
||||
export const ElementTypeTracingMarker = 14;
|
||||
export const ElementTypeVirtual = 15;
|
||||
export const ElementTypeViewTransition = 16;
|
||||
|
||||
// Different types of elements displayed in the Elements tree.
|
||||
// These types may be used to visually distinguish types,
|
||||
@@ -66,7 +67,8 @@ export type ElementType =
|
||||
| 12
|
||||
| 13
|
||||
| 14
|
||||
| 15;
|
||||
| 15
|
||||
| 16;
|
||||
|
||||
// WARNING
|
||||
// The values below are referenced by ComponentFilters (which are saved via localStorage).
|
||||
|
||||
+1
@@ -648,6 +648,7 @@ export function installHook(
|
||||
checkDCE,
|
||||
onCommitFiberUnmount,
|
||||
onCommitFiberRoot,
|
||||
// React v18.0+
|
||||
onPostCommitFiberRoot,
|
||||
setStrictMode,
|
||||
|
||||
|
||||
+4
@@ -24,6 +24,7 @@ import {
|
||||
REACT_SUSPENSE_LIST_TYPE,
|
||||
REACT_SUSPENSE_TYPE,
|
||||
REACT_TRACING_MARKER_TYPE,
|
||||
REACT_VIEW_TRANSITION_TYPE,
|
||||
} from 'shared/ReactSymbols';
|
||||
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
@@ -678,6 +679,7 @@ function typeOfWithLegacyElementSymbol(object: any): mixed {
|
||||
case REACT_STRICT_MODE_TYPE:
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
case REACT_VIEW_TRANSITION_TYPE:
|
||||
return type;
|
||||
default:
|
||||
const $$typeofType = type && type.$$typeof;
|
||||
@@ -739,6 +741,8 @@ export function getDisplayNameForReactElement(
|
||||
return 'Suspense';
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return 'SuspenseList';
|
||||
case REACT_VIEW_TRANSITION_TYPE:
|
||||
return 'ViewTransition';
|
||||
case REACT_TRACING_MARKER_TYPE:
|
||||
return 'TracingMarker';
|
||||
default:
|
||||
|
||||
@@ -174,6 +174,9 @@ const appServer = new WebpackDevServer(
|
||||
port: 8080,
|
||||
client: {
|
||||
logging: 'warn',
|
||||
overlay: {
|
||||
warnings: false,
|
||||
},
|
||||
},
|
||||
static: {
|
||||
directory: __dirname,
|
||||
@@ -189,6 +192,9 @@ const e2eRegressionAppServer = new WebpackDevServer(
|
||||
port: 8181,
|
||||
client: {
|
||||
logging: 'warn',
|
||||
overlay: {
|
||||
warnings: false,
|
||||
},
|
||||
},
|
||||
static: {
|
||||
publicPath: '/dist/',
|
||||
|
||||
@@ -11,6 +11,7 @@ import hyphenateStyleName from '../shared/hyphenateStyleName';
|
||||
import warnValidStyle from '../shared/warnValidStyle';
|
||||
import isUnitlessNumber from '../shared/isUnitlessNumber';
|
||||
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
|
||||
|
||||
/**
|
||||
* Operations for dealing with CSS properties.
|
||||
@@ -144,12 +145,14 @@ export function setValueForStyles(node, styles, prevStyles) {
|
||||
} else {
|
||||
style[styleName] = '';
|
||||
}
|
||||
trackHostMutation();
|
||||
}
|
||||
}
|
||||
for (const styleName in styles) {
|
||||
const value = styles[styleName];
|
||||
if (styles.hasOwnProperty(styleName) && prevStyles[styleName] !== value) {
|
||||
setValueForStyle(style, styleName, value);
|
||||
trackHostMutation();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ import isAttributeNameSafe from '../shared/isAttributeNameSafe';
|
||||
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
|
||||
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
|
||||
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
|
||||
|
||||
/**
|
||||
* Get the value for a attribute on a node. Only used in DEV for SSR validation.
|
||||
@@ -217,6 +218,8 @@ export function setValueForPropertyOnCustomComponent(
|
||||
}
|
||||
}
|
||||
|
||||
trackHostMutation();
|
||||
|
||||
if (name in (node: any)) {
|
||||
(node: any)[name] = value;
|
||||
return;
|
||||
|
||||
+73
-30
@@ -63,6 +63,8 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN
|
||||
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
|
||||
import sanitizeURL from '../shared/sanitizeURL';
|
||||
|
||||
import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
|
||||
|
||||
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
mediaEventTypes,
|
||||
@@ -363,6 +365,8 @@ function setProp(
|
||||
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
||||
setTextContent(domElement, '' + value);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -386,7 +390,7 @@ function setProp(
|
||||
}
|
||||
case 'style': {
|
||||
setValueForStyles(domElement, value, prevValue);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
// These attributes accept URLs. These must not allow javascript: URLS.
|
||||
case 'data':
|
||||
@@ -524,7 +528,7 @@ function setProp(
|
||||
}
|
||||
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'onScroll': {
|
||||
if (value != null) {
|
||||
@@ -533,7 +537,7 @@ function setProp(
|
||||
}
|
||||
listenToNonDelegatedEvent('scroll', domElement);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'onScrollEnd': {
|
||||
if (value != null) {
|
||||
@@ -542,7 +546,7 @@ function setProp(
|
||||
}
|
||||
listenToNonDelegatedEvent('scrollend', domElement);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'dangerouslySetInnerHTML': {
|
||||
if (value != null) {
|
||||
@@ -849,7 +853,7 @@ function setProp(
|
||||
}
|
||||
case 'innerText':
|
||||
case 'textContent':
|
||||
break;
|
||||
return;
|
||||
case 'popoverTarget':
|
||||
if (__DEV__) {
|
||||
if (
|
||||
@@ -879,12 +883,16 @@ function setProp(
|
||||
) {
|
||||
warnForInvalidEventListener(key, value);
|
||||
}
|
||||
// Updating events doesn't affect the visuals.
|
||||
return;
|
||||
} else {
|
||||
const attributeName = getAttributeAlias(key);
|
||||
setValueForAttribute(domElement, attributeName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// To avoid marking things as host mutations we do early returns above.
|
||||
trackHostMutation();
|
||||
}
|
||||
|
||||
function setPropOnCustomElement(
|
||||
@@ -898,7 +906,7 @@ function setPropOnCustomElement(
|
||||
switch (key) {
|
||||
case 'style': {
|
||||
setValueForStyles(domElement, value, prevValue);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'dangerouslySetInnerHTML': {
|
||||
if (value != null) {
|
||||
@@ -927,6 +935,8 @@ function setPropOnCustomElement(
|
||||
} else if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
||||
setTextContent(domElement, '' + value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -937,7 +947,7 @@ function setPropOnCustomElement(
|
||||
}
|
||||
listenToNonDelegatedEvent('scroll', domElement);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'onScrollEnd': {
|
||||
if (value != null) {
|
||||
@@ -946,7 +956,7 @@ function setPropOnCustomElement(
|
||||
}
|
||||
listenToNonDelegatedEvent('scrollend', domElement);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'onClick': {
|
||||
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
||||
@@ -956,29 +966,34 @@ function setPropOnCustomElement(
|
||||
}
|
||||
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'suppressContentEditableWarning':
|
||||
case 'suppressHydrationWarning':
|
||||
case 'innerHTML':
|
||||
case 'ref': {
|
||||
// Noop
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case 'innerText': // Properties
|
||||
case 'textContent':
|
||||
break;
|
||||
return;
|
||||
// Fall through
|
||||
default: {
|
||||
if (registrationNameDependencies.hasOwnProperty(key)) {
|
||||
if (__DEV__ && value != null && typeof value !== 'function') {
|
||||
warnForInvalidEventListener(key, value);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
setValueForPropertyOnCustomComponent(domElement, key, value);
|
||||
// We track mutations inside this call.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// To avoid marking things as host mutations we do early returns above.
|
||||
trackHostMutation();
|
||||
}
|
||||
|
||||
export function setInitialProperties(
|
||||
@@ -1430,26 +1445,44 @@ export function updateProperties(
|
||||
) {
|
||||
switch (propKey) {
|
||||
case 'type': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
type = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'name': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
name = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'checked': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
checked = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'defaultChecked': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
defaultChecked = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
value = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'defaultValue': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
defaultValue = nextProp;
|
||||
break;
|
||||
}
|
||||
@@ -1553,8 +1586,9 @@ export function updateProperties(
|
||||
}
|
||||
// Fallthrough
|
||||
default: {
|
||||
if (!nextProps.hasOwnProperty(propKey))
|
||||
if (!nextProps.hasOwnProperty(propKey)) {
|
||||
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1568,15 +1602,24 @@ export function updateProperties(
|
||||
) {
|
||||
switch (propKey) {
|
||||
case 'value': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
value = nextProp;
|
||||
// This is handled by updateSelect below.
|
||||
break;
|
||||
}
|
||||
case 'defaultValue': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
defaultValue = nextProp;
|
||||
break;
|
||||
}
|
||||
case 'multiple': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
multiple = nextProp;
|
||||
// TODO: Just move the special case in here from setProp.
|
||||
}
|
||||
@@ -1635,11 +1678,17 @@ export function updateProperties(
|
||||
) {
|
||||
switch (propKey) {
|
||||
case 'value': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
value = nextProp;
|
||||
// This is handled by updateTextarea below.
|
||||
break;
|
||||
}
|
||||
case 'defaultValue': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
defaultValue = nextProp;
|
||||
break;
|
||||
}
|
||||
@@ -1703,6 +1752,9 @@ export function updateProperties(
|
||||
) {
|
||||
switch (propKey) {
|
||||
case 'selected': {
|
||||
if (nextProp !== lastProp) {
|
||||
trackHostMutation();
|
||||
}
|
||||
// TODO: Remove support for selected on option.
|
||||
(domElement: any).selected =
|
||||
nextProp &&
|
||||
@@ -2510,26 +2562,17 @@ function diffHydratedGenericElement(
|
||||
);
|
||||
}
|
||||
}
|
||||
hydrateSanitizedAttribute(
|
||||
domElement,
|
||||
propKey,
|
||||
propKey,
|
||||
null,
|
||||
extraAttributes,
|
||||
serverDifferences,
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
hydrateSanitizedAttribute(
|
||||
domElement,
|
||||
propKey,
|
||||
propKey,
|
||||
value,
|
||||
extraAttributes,
|
||||
serverDifferences,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
hydrateSanitizedAttribute(
|
||||
domElement,
|
||||
propKey,
|
||||
propKey,
|
||||
value,
|
||||
extraAttributes,
|
||||
serverDifferences,
|
||||
);
|
||||
continue;
|
||||
case 'action':
|
||||
case 'formAction': {
|
||||
const serverValue = domElement.getAttribute(propKey);
|
||||
|
||||
+452
-7
@@ -120,7 +120,14 @@ export type Props = {
|
||||
hidden?: boolean,
|
||||
suppressHydrationWarning?: boolean,
|
||||
dangerouslySetInnerHTML?: mixed,
|
||||
style?: {display?: string, ...},
|
||||
style?: {
|
||||
display?: string,
|
||||
viewTransitionName?: string,
|
||||
'view-transition-name'?: string,
|
||||
viewTransitionClass?: string,
|
||||
'view-transition-class'?: string,
|
||||
...
|
||||
},
|
||||
bottom?: null | number,
|
||||
left?: null | number,
|
||||
right?: null | number,
|
||||
@@ -149,6 +156,7 @@ export type EventTargetChildElement = {
|
||||
},
|
||||
...
|
||||
};
|
||||
|
||||
export type Container =
|
||||
| interface extends Element {_reactRootContainer?: FiberRoot}
|
||||
| interface extends Document {_reactRootContainer?: FiberRoot}
|
||||
@@ -179,6 +187,14 @@ export type RendererInspectionConfig = $ReadOnly<{}>;
|
||||
|
||||
export type TransitionStatus = FormStatus;
|
||||
|
||||
export type ViewTransitionInstance = {
|
||||
name: string,
|
||||
group: Animatable,
|
||||
imagePair: Animatable,
|
||||
old: Animatable,
|
||||
new: Animatable,
|
||||
};
|
||||
|
||||
type SelectionInformation = {
|
||||
focusedElem: null | HTMLElement,
|
||||
selectionRange: mixed,
|
||||
@@ -785,10 +801,20 @@ export function appendChildToContainer(
|
||||
let parentNode;
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
parentNode = (container.parentNode: any);
|
||||
parentNode.insertBefore(child, container);
|
||||
if (supportsMoveBefore) {
|
||||
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
|
||||
parentNode.moveBefore(child, container);
|
||||
} else {
|
||||
parentNode.insertBefore(child, container);
|
||||
}
|
||||
} else {
|
||||
parentNode = container;
|
||||
parentNode.appendChild(child);
|
||||
if (supportsMoveBefore) {
|
||||
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
|
||||
parentNode.moveBefore(child, null);
|
||||
} else {
|
||||
parentNode.appendChild(child);
|
||||
}
|
||||
}
|
||||
// This container might be used for a portal.
|
||||
// If something inside a portal is clicked, that click should bubble
|
||||
@@ -827,9 +853,19 @@ export function insertInContainerBefore(
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
(container.parentNode: any).insertBefore(child, beforeChild);
|
||||
if (supportsMoveBefore) {
|
||||
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
|
||||
(container.parentNode: any).moveBefore(child, beforeChild);
|
||||
} else {
|
||||
(container.parentNode: any).insertBefore(child, beforeChild);
|
||||
}
|
||||
} else {
|
||||
container.insertBefore(child, beforeChild);
|
||||
if (supportsMoveBefore) {
|
||||
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
|
||||
container.moveBefore(child, beforeChild);
|
||||
} else {
|
||||
container.insertBefore(child, beforeChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -978,6 +1014,392 @@ export function unhideTextInstance(
|
||||
textInstance.nodeValue = text;
|
||||
}
|
||||
|
||||
export function applyViewTransitionName(
|
||||
instance: Instance,
|
||||
name: string,
|
||||
className: ?string,
|
||||
): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionName = name;
|
||||
if (className != null) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass = className;
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreViewTransitionName(
|
||||
instance: Instance,
|
||||
props: Props,
|
||||
): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
const styleProp = props[STYLE];
|
||||
const viewTransitionName =
|
||||
styleProp != null
|
||||
? styleProp.hasOwnProperty('viewTransitionName')
|
||||
? styleProp.viewTransitionName
|
||||
: styleProp.hasOwnProperty('view-transition-name')
|
||||
? styleProp['view-transition-name']
|
||||
: null
|
||||
: null;
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionName =
|
||||
viewTransitionName == null || typeof viewTransitionName === 'boolean'
|
||||
? ''
|
||||
: // The value would've errored already if it wasn't safe.
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
('' + viewTransitionName).trim();
|
||||
const viewTransitionClass =
|
||||
styleProp != null
|
||||
? styleProp.hasOwnProperty('viewTransitionClass')
|
||||
? styleProp.viewTransitionClass
|
||||
: styleProp.hasOwnProperty('view-transition-class')
|
||||
? styleProp['view-transition-class']
|
||||
: null
|
||||
: null;
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass =
|
||||
viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
|
||||
? ''
|
||||
: // The value would've errored already if it wasn't safe.
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
('' + viewTransitionClass).trim();
|
||||
}
|
||||
|
||||
export function cancelViewTransitionName(
|
||||
instance: Instance,
|
||||
oldName: string,
|
||||
props: Props,
|
||||
): void {
|
||||
// To cancel the "new" state and paint this instance as part of the parent, all we have to do
|
||||
// is remove the view-transition-name before we exit startViewTransition.
|
||||
restoreViewTransitionName(instance, props);
|
||||
// There isn't a way to cancel an "old" state but what we can do is hide it by animating it.
|
||||
// Since it is already removed from the old state of the parent, this technique only works
|
||||
// if the parent also isn't transitioning. Therefore we should only cancel the root most
|
||||
// ViewTransitions.
|
||||
const documentElement = instance.ownerDocument.documentElement;
|
||||
if (documentElement !== null) {
|
||||
documentElement.animate(
|
||||
{opacity: [0, 0], pointerEvents: ['none', 'none']},
|
||||
{
|
||||
duration: 0,
|
||||
fill: 'forwards',
|
||||
pseudoElement: '::view-transition-group(' + oldName + ')',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelRootViewTransitionName(rootContainer: Container): void {
|
||||
const documentElement: null | HTMLElement =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any).documentElement
|
||||
: rootContainer.ownerDocument.documentElement;
|
||||
if (
|
||||
documentElement !== null &&
|
||||
// $FlowFixMe[prop-missing]
|
||||
documentElement.style.viewTransitionName === ''
|
||||
) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
documentElement.style.viewTransitionName = 'none';
|
||||
documentElement.animate(
|
||||
{opacity: [0, 0], pointerEvents: ['none', 'none']},
|
||||
{
|
||||
duration: 0,
|
||||
fill: 'forwards',
|
||||
pseudoElement: '::view-transition-group(root)',
|
||||
},
|
||||
);
|
||||
// By default the root ::view-transition selector captures all pointer events,
|
||||
// which means nothing gets interactive. We want to let whatever is not animating
|
||||
// remain interactive during the transition. To do that, we set the size to nothing
|
||||
// so that the transition doesn't capture any clicks. We don't set pointer-events
|
||||
// on this one as that would apply to all running transitions. This lets animations
|
||||
// that are running to block clicks so that they don't end up incorrectly hitting
|
||||
// whatever is below the animation.
|
||||
documentElement.animate(
|
||||
{width: [0, 0], height: [0, 0]},
|
||||
{
|
||||
duration: 0,
|
||||
fill: 'forwards',
|
||||
pseudoElement: '::view-transition',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreRootViewTransitionName(rootContainer: Container): void {
|
||||
const documentElement: null | HTMLElement =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any).documentElement
|
||||
: rootContainer.ownerDocument.documentElement;
|
||||
if (
|
||||
documentElement !== null &&
|
||||
// $FlowFixMe[prop-missing]
|
||||
documentElement.style.viewTransitionName === 'none'
|
||||
) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
documentElement.style.viewTransitionName = '';
|
||||
}
|
||||
}
|
||||
|
||||
export type InstanceMeasurement = {
|
||||
rect: ClientRect | DOMRect,
|
||||
abs: boolean, // is absolutely positioned
|
||||
clip: boolean, // is a clipping parent
|
||||
view: boolean, // is in viewport bounds
|
||||
};
|
||||
|
||||
export function measureInstance(instance: Instance): InstanceMeasurement {
|
||||
const ownerWindow = instance.ownerDocument.defaultView;
|
||||
const rect = instance.getBoundingClientRect();
|
||||
const computedStyle = getComputedStyle(instance);
|
||||
return {
|
||||
rect: rect,
|
||||
abs:
|
||||
// Absolutely positioned instances don't contribute their size to the parent.
|
||||
computedStyle.position === 'absolute' ||
|
||||
computedStyle.position === 'fixed',
|
||||
clip:
|
||||
// If a ViewTransition boundary acts as a clipping parent group we should
|
||||
// always mark it to animate if its children do so that we can clip them.
|
||||
// This doesn't actually have any effect yet until browsers implement
|
||||
// layered capture and nested view transitions.
|
||||
computedStyle.clipPath !== 'none' ||
|
||||
computedStyle.overflow !== 'visible' ||
|
||||
computedStyle.filter !== 'none' ||
|
||||
computedStyle.mask !== 'none' ||
|
||||
computedStyle.mask !== 'none' ||
|
||||
computedStyle.borderRadius !== '0px',
|
||||
view:
|
||||
// If the instance was within the bounds of the viewport. We don't care as
|
||||
// much about if it was fully occluded because then it can still pop out.
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= ownerWindow.innerHeight &&
|
||||
rect.left <= ownerWindow.innerWidth,
|
||||
};
|
||||
}
|
||||
|
||||
export function wasInstanceInViewport(
|
||||
measurement: InstanceMeasurement,
|
||||
): boolean {
|
||||
return measurement.view;
|
||||
}
|
||||
|
||||
export function hasInstanceChanged(
|
||||
oldMeasurement: InstanceMeasurement,
|
||||
newMeasurement: InstanceMeasurement,
|
||||
): boolean {
|
||||
// Note: This is not guaranteed from the same instance in the case that the Instance of the
|
||||
// ViewTransition swaps out but it's still the same ViewTransition instance.
|
||||
if (newMeasurement.clip) {
|
||||
// If we're a clipping parent, we always animate if any of our children do so that we can clip
|
||||
// them. This doesn't yet until browsers implement layered capture and nested view transitions.
|
||||
return true;
|
||||
}
|
||||
const oldRect = oldMeasurement.rect;
|
||||
const newRect = newMeasurement.rect;
|
||||
return (
|
||||
oldRect.y !== newRect.y ||
|
||||
oldRect.x !== newRect.x ||
|
||||
oldRect.height !== newRect.height ||
|
||||
oldRect.width !== newRect.width
|
||||
);
|
||||
}
|
||||
|
||||
export function hasInstanceAffectedParent(
|
||||
oldMeasurement: InstanceMeasurement,
|
||||
newMeasurement: InstanceMeasurement,
|
||||
): boolean {
|
||||
// Note: This is not guaranteed from the same instance in the case that the Instance of the
|
||||
// ViewTransition swaps out but it's still the same ViewTransition instance.
|
||||
// If the instance has resized, it might have affected the parent layout.
|
||||
if (newMeasurement.abs) {
|
||||
// Absolutely positioned elements don't affect the parent layout, unless they
|
||||
// previously were not absolutely positioned.
|
||||
return !oldMeasurement.abs;
|
||||
}
|
||||
const oldRect = oldMeasurement.rect;
|
||||
const newRect = newMeasurement.rect;
|
||||
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
|
||||
}
|
||||
|
||||
// How long to wait for new fonts to load before just committing anyway.
|
||||
// This freezes the screen. It needs to be short enough that it doesn't cause too much of
|
||||
// an issue when it's a new load and slow, yet long enough that you have a chance to load
|
||||
// it. Otherwise we wait for no reason. The assumption here is that you likely have
|
||||
// either cached the font or preloaded it earlier.
|
||||
const SUSPENSEY_FONT_TIMEOUT = 500;
|
||||
|
||||
export function startViewTransition(
|
||||
rootContainer: Container,
|
||||
mutationCallback: () => void,
|
||||
layoutCallback: () => void,
|
||||
afterMutationCallback: () => void,
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
): boolean {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any)
|
||||
: rootContainer.ownerDocument;
|
||||
try {
|
||||
// $FlowFixMe[prop-missing]
|
||||
const transition = ownerDocument.startViewTransition({
|
||||
update() {
|
||||
// Note: We read the existence of a pending navigation before we apply the
|
||||
// mutations. That way we're not waiting on a navigation that we spawned
|
||||
// from this update. Only navigations that started before this commit.
|
||||
const ownerWindow = ownerDocument.defaultView;
|
||||
const pendingNavigation =
|
||||
ownerWindow.navigation && ownerWindow.navigation.transition;
|
||||
// $FlowFixMe[prop-missing]
|
||||
const previousFontLoadingStatus = ownerDocument.fonts.status;
|
||||
mutationCallback();
|
||||
if (previousFontLoadingStatus === 'loaded') {
|
||||
// Force layout calculation to trigger font loading.
|
||||
// eslint-disable-next-line ft-flow/no-unused-expressions
|
||||
(ownerDocument.documentElement: any).clientHeight;
|
||||
if (
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.fonts.status === 'loading'
|
||||
) {
|
||||
// The mutation lead to new fonts being loaded. We should wait on them before continuing.
|
||||
// This avoids waiting for potentially unrelated fonts that were already loading before.
|
||||
// Either in an earlier transition or as part of a sync optimistic state. This doesn't
|
||||
// include preloads that happened earlier.
|
||||
const fontsReady = Promise.race([
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.fonts.ready,
|
||||
new Promise(resolve =>
|
||||
setTimeout(resolve, SUSPENSEY_FONT_TIMEOUT),
|
||||
),
|
||||
]).then(layoutCallback, layoutCallback);
|
||||
const allReady = pendingNavigation
|
||||
? Promise.allSettled([pendingNavigation.finished, fontsReady])
|
||||
: fontsReady;
|
||||
return allReady.then(afterMutationCallback, afterMutationCallback);
|
||||
}
|
||||
}
|
||||
layoutCallback();
|
||||
if (pendingNavigation) {
|
||||
return pendingNavigation.finished.then(
|
||||
afterMutationCallback,
|
||||
afterMutationCallback,
|
||||
);
|
||||
} else {
|
||||
afterMutationCallback();
|
||||
}
|
||||
},
|
||||
types: null, // TODO: Provide types.
|
||||
});
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.__reactViewTransition = transition;
|
||||
if (__DEV__) {
|
||||
transition.ready.then(undefined, (reason: mixed) => {
|
||||
if (
|
||||
typeof reason === 'object' &&
|
||||
reason !== null &&
|
||||
reason.name === 'TimeoutError'
|
||||
) {
|
||||
console.error(
|
||||
'A ViewTransition timed out because a Navigation stalled. ' +
|
||||
'This can happen if a Navigation is blocked on React itself. ' +
|
||||
"Such as if it's resolved inside useEffect. " +
|
||||
'This can be solved by moving the resolution to useLayoutEffect.',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
transition.ready.then(spawnedWorkCallback, spawnedWorkCallback);
|
||||
transition.finished.then(() => {
|
||||
// $FlowFixMe[prop-missing]
|
||||
ownerDocument.__reactViewTransition = null;
|
||||
passiveCallback();
|
||||
});
|
||||
return true;
|
||||
} catch (x) {
|
||||
// We use the error as feature detection.
|
||||
// The only thing that should throw is if startViewTransition is missing
|
||||
// or if it doesn't accept the object form. Other errors are async.
|
||||
// I.e. it's before the View Transitions v2 spec. We only support View
|
||||
// Transitions v2 otherwise we fallback to not animating to ensure that
|
||||
// we're not animating with the wrong animation mapped.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface ViewTransitionPseudoElementType extends Animatable {
|
||||
_scope: HTMLElement;
|
||||
_selector: string;
|
||||
}
|
||||
|
||||
function ViewTransitionPseudoElement(
|
||||
this: ViewTransitionPseudoElementType,
|
||||
pseudo: string,
|
||||
name: string,
|
||||
) {
|
||||
// TODO: Get the owner document from the root container.
|
||||
this._scope = (document.documentElement: any);
|
||||
this._selector = '::view-transition-' + pseudo + '(' + name + ')';
|
||||
}
|
||||
// $FlowFixMe[prop-missing]
|
||||
ViewTransitionPseudoElement.prototype.animate = function (
|
||||
this: ViewTransitionPseudoElementType,
|
||||
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
|
||||
options?: number | KeyframeAnimationOptions,
|
||||
): Animation {
|
||||
const opts: any =
|
||||
typeof options === 'number'
|
||||
? {
|
||||
duration: options,
|
||||
}
|
||||
: Object.assign(({}: KeyframeAnimationOptions), options);
|
||||
opts.pseudoElement = this._selector;
|
||||
// TODO: Handle multiple child instances.
|
||||
return this._scope.animate(keyframes, opts);
|
||||
};
|
||||
// $FlowFixMe[prop-missing]
|
||||
ViewTransitionPseudoElement.prototype.getAnimations = function (
|
||||
this: ViewTransitionPseudoElementType,
|
||||
options?: GetAnimationsOptions,
|
||||
): Animation[] {
|
||||
const scope = this._scope;
|
||||
const selector = this._selector;
|
||||
const animations = scope.getAnimations({subtree: true});
|
||||
const result = [];
|
||||
for (let i = 0; i < animations.length; i++) {
|
||||
const effect: null | {
|
||||
target?: Element,
|
||||
pseudoElement?: string,
|
||||
...
|
||||
} = (animations[i].effect: any);
|
||||
// TODO: Handle multiple child instances.
|
||||
if (
|
||||
effect !== null &&
|
||||
effect.target === scope &&
|
||||
effect.pseudoElement === selector
|
||||
) {
|
||||
result.push(animations[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export function createViewTransitionInstance(
|
||||
name: string,
|
||||
): ViewTransitionInstance {
|
||||
return {
|
||||
name: name,
|
||||
group: new (ViewTransitionPseudoElement: any)('group', name),
|
||||
imagePair: new (ViewTransitionPseudoElement: any)('image-pair', name),
|
||||
old: new (ViewTransitionPseudoElement: any)('old', name),
|
||||
new: new (ViewTransitionPseudoElement: any)('new', name),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearContainer(container: Container): void {
|
||||
const nodeType = container.nodeType;
|
||||
if (nodeType === DOCUMENT_NODE) {
|
||||
@@ -1139,7 +1561,9 @@ export function canHydrateInstance(
|
||||
} else if (
|
||||
rel !== anyProps.rel ||
|
||||
element.getAttribute('href') !==
|
||||
(anyProps.href == null ? null : anyProps.href) ||
|
||||
(anyProps.href == null || anyProps.href === ''
|
||||
? null
|
||||
: anyProps.href) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
|
||||
element.getAttribute('title') !==
|
||||
@@ -2984,7 +3408,7 @@ export function hydrateHoistable(
|
||||
const node = nodes[i];
|
||||
if (
|
||||
node.getAttribute('href') !==
|
||||
(props.href == null ? null : props.href) ||
|
||||
(props.href == null || props.href === '' ? null : props.href) ||
|
||||
node.getAttribute('rel') !==
|
||||
(props.rel == null ? null : props.rel) ||
|
||||
node.getAttribute('title') !==
|
||||
@@ -3449,6 +3873,27 @@ export function suspendResource(
|
||||
}
|
||||
}
|
||||
|
||||
export function suspendOnActiveViewTransition(rootContainer: Container): void {
|
||||
if (suspendedState === null) {
|
||||
throw new Error(
|
||||
'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
|
||||
);
|
||||
}
|
||||
const state = suspendedState;
|
||||
const ownerDocument =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? rootContainer
|
||||
: rootContainer.ownerDocument;
|
||||
// $FlowFixMe[prop-missing]
|
||||
const activeViewTransition = ownerDocument.__reactViewTransition;
|
||||
if (activeViewTransition == null) {
|
||||
return;
|
||||
}
|
||||
state.count++;
|
||||
const ping = onUnsuspend.bind(state);
|
||||
activeViewTransition.finished.then(ping, ping);
|
||||
}
|
||||
|
||||
export function waitForCommitToBeReady(): null | ((() => void) => () => void) {
|
||||
if (suspendedState === null) {
|
||||
throw new Error(
|
||||
|
||||
+29
-33
@@ -13,6 +13,8 @@ const React = require('react');
|
||||
const ReactDOMClient = require('react-dom/client');
|
||||
const ReactDOMServer = require('react-dom/server');
|
||||
const act = require('internal-test-utils').act;
|
||||
const assertConsoleErrorDev =
|
||||
require('internal-test-utils').assertConsoleErrorDev;
|
||||
|
||||
describe('CSSPropertyOperations', () => {
|
||||
it('should automatically append `px` to relevant styles', () => {
|
||||
@@ -103,15 +105,14 @@ describe('CSSPropertyOperations', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
'Unsupported style property background-color. Did you mean backgroundColor?' +
|
||||
'\n in div (at **)' +
|
||||
'\n in Comp (at **)',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it('should warn when updating hyphenated style names', async () => {
|
||||
@@ -132,11 +133,10 @@ describe('CSSPropertyOperations', () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp style={styles} />);
|
||||
});
|
||||
}).toErrorDev([
|
||||
await act(() => {
|
||||
root.render(<Comp style={styles} />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
'Unsupported style property -ms-transform. Did you mean msTransform?' +
|
||||
'\n in div (at **)' +
|
||||
'\n in Comp (at **)',
|
||||
@@ -165,11 +165,10 @@ describe('CSSPropertyOperations', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
}).toErrorDev([
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
// msTransform is correct already and shouldn't warn
|
||||
'Unsupported vendor-prefixed style property oTransform. ' +
|
||||
'Did you mean OTransform?' +
|
||||
@@ -202,11 +201,10 @@ describe('CSSPropertyOperations', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
}).toErrorDev([
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
"Style property values shouldn't contain a semicolon. " +
|
||||
'Try "backgroundColor: blue" instead.' +
|
||||
'\n in div (at **)' +
|
||||
@@ -229,15 +227,14 @@ describe('CSSPropertyOperations', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
'`NaN` is an invalid value for the `fontSize` css style property.' +
|
||||
'\n in div (at **)' +
|
||||
'\n in Comp (at **)',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not warn when setting CSS custom properties', async () => {
|
||||
@@ -265,15 +262,14 @@ describe('CSSPropertyOperations', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
}).toErrorDev(
|
||||
await act(() => {
|
||||
root.render(<Comp />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
'`Infinity` is an invalid value for the `fontSize` css style property.' +
|
||||
'\n in div (at **)' +
|
||||
'\n in Comp (at **)',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not add units to CSS custom properties', async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user