Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c10c8f7e4 | ||
|
|
a688a3d18c | ||
|
|
2c8725fdfd | ||
|
|
03613cd68c | ||
|
|
2af6822c21 | ||
|
|
24d8716e36 | ||
|
|
94913cbffe | ||
|
|
2d8e7f1ce3 | ||
|
|
6a0ab4d2dd | ||
|
|
03ee29da2f | ||
|
|
cdbd55f440 | ||
|
|
b546603bcb | ||
|
|
7fccd6b5a3 | ||
|
|
d29087523a | ||
|
|
d343c39cce | ||
|
|
1ecd99c774 | ||
|
|
c55ffb5ca3 | ||
|
|
a49952b303 | ||
|
|
4bcf67e746 | ||
|
|
41b3e9a670 | ||
|
|
195fd2286b | ||
|
|
d87298ae16 | ||
|
|
be3fb29904 | ||
|
|
23e5edd05c | ||
|
|
3926e2438f | ||
|
|
6baff7ac76 | ||
|
|
bef88f7c11 | ||
|
|
01c4d03d84 | ||
|
|
cbc4d40663 | ||
|
|
db71391c5c | ||
|
|
4cf906380d | ||
|
|
eac3c95537 | ||
|
|
35a81cecf7 | ||
|
|
4028aaa50c | ||
|
|
f0fbb0d199 | ||
|
|
bb8a76c6cc | ||
|
|
fae15df40e | ||
|
|
53daaf5aba | ||
|
|
4a3d993e52 | ||
|
|
3e1abcc8d7 | ||
|
|
c18662405c | ||
|
|
583e200332 | ||
|
|
8a83073753 |
@@ -593,6 +593,7 @@ module.exports = {
|
||||
mixin$Animatable: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
Partial: 'readonly',
|
||||
PerformanceMeasureOptions: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
|
||||
3
.github/workflows/runtime_build_and_test.yml
vendored
3
.github/workflows/runtime_build_and_test.yml
vendored
@@ -278,6 +278,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd compiler install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: node --version
|
||||
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
|
||||
|
||||
# Hardcoded to improve parallelism
|
||||
@@ -445,6 +446,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: node --version
|
||||
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
|
||||
|
||||
test_build_devtools:
|
||||
@@ -489,6 +491,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: node --version
|
||||
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
|
||||
|
||||
process_artifacts_combined:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,6 +24,8 @@ chrome-user-data
|
||||
*.swp
|
||||
*.swo
|
||||
/tmp
|
||||
/.worktrees
|
||||
.claude/*.local.*
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn snap:*)",
|
||||
"Bash(yarn snap:build)",
|
||||
"Bash(node scripts/enable-feature-flag.js:*)"
|
||||
],
|
||||
"deny": [],
|
||||
2
compiler/.gitignore
vendored
2
compiler/.gitignore
vendored
@@ -8,7 +8,9 @@ dist
|
||||
.vscode
|
||||
!packages/playground/.vscode
|
||||
testfilter.txt
|
||||
.claude/settings.local.json
|
||||
|
||||
# forgive
|
||||
*.vsix
|
||||
.vscode-test
|
||||
|
||||
|
||||
221
compiler/CLAUDE.md
Normal file
221
compiler/CLAUDE.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# React Compiler Knowledge Base
|
||||
|
||||
This document contains knowledge about the React Compiler gathered during development sessions. It serves as a reference for understanding the codebase architecture and key concepts.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `packages/babel-plugin-react-compiler/` - Main compiler package
|
||||
- `src/HIR/` - High-level Intermediate Representation types and utilities
|
||||
- `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
|
||||
- `src/Validation/` - Validation passes that check for errors
|
||||
- `src/Entrypoint/Pipeline.ts` - Main compilation pipeline with pass ordering
|
||||
- `src/__tests__/fixtures/compiler/` - Test fixtures
|
||||
- `error.todo-*.js` - Unsupported feature, correctly throws Todo error (graceful bailout)
|
||||
- `error.bug-*.js` - Known bug, throws wrong error type or incorrect behavior
|
||||
- `*.expect.md` - Expected output for each fixture
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
yarn snap
|
||||
|
||||
# Run tests matching a pattern
|
||||
# Example: yarn snap -p 'error.*'
|
||||
yarn snap -p <pattern>
|
||||
|
||||
# Run a single fixture in debug mode. Use the path relative to the __tests__/fixtures/compiler directory
|
||||
# For each step of compilation, outputs the step name and state of the compiled program
|
||||
# Example: yarn snap -p simple.js -d
|
||||
yarn snap -p <file-basename> -d
|
||||
|
||||
# Update fixture outputs (also works with -p)
|
||||
yarn snap -u
|
||||
```
|
||||
|
||||
## Version Control
|
||||
|
||||
This repository uses Sapling (`sl`) for version control. Sapling is similar to Mercurial: there is not staging area, but new/deleted files must be explicitlyu added/removed.
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
sl status
|
||||
|
||||
# Add new files, remove deleted files
|
||||
sl addremove
|
||||
|
||||
# Commit all changes
|
||||
sl commit -m "Your commit message"
|
||||
|
||||
# Commit with multi-line message using heredoc
|
||||
sl commit -m "$(cat <<'EOF'
|
||||
Summary line
|
||||
|
||||
Detailed description here
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### HIR (High-level Intermediate Representation)
|
||||
|
||||
The compiler converts source code to HIR for analysis. Key types in `src/HIR/HIR.ts`:
|
||||
|
||||
- **HIRFunction** - A function being compiled
|
||||
- `body.blocks` - Map of BasicBlocks
|
||||
- `context` - Captured variables from outer scope
|
||||
- `params` - Function parameters
|
||||
- `returns` - The function's return place
|
||||
- `aliasingEffects` - Effects that describe the function's behavior when called
|
||||
|
||||
- **Instruction** - A single operation
|
||||
- `lvalue` - The place being assigned to
|
||||
- `value` - The instruction kind (CallExpression, FunctionExpression, LoadLocal, etc.)
|
||||
- `effects` - Array of AliasingEffects for this instruction
|
||||
|
||||
- **Terminal** - Block terminators (return, branch, etc.)
|
||||
- `effects` - Array of AliasingEffects
|
||||
|
||||
- **Place** - A reference to a value
|
||||
- `identifier.id` - Unique IdentifierId
|
||||
|
||||
- **Phi nodes** - Join points for values from different control flow paths
|
||||
- Located at `block.phis`
|
||||
- `phi.place` - The result place
|
||||
- `phi.operands` - Map of predecessor block to source place
|
||||
|
||||
### AliasingEffects System
|
||||
|
||||
Effects describe data flow and operations. Defined in `src/Inference/AliasingEffects.ts`:
|
||||
|
||||
**Data Flow Effects:**
|
||||
- `Impure` - Marks a place as containing an impure value (e.g., Date.now() result, ref.current)
|
||||
- `Capture a -> b` - Value from `a` is captured into `b` (mutable capture)
|
||||
- `Alias a -> b` - `b` aliases `a`
|
||||
- `ImmutableCapture a -> b` - Immutable capture (like Capture but read-only)
|
||||
- `Assign a -> b` - Direct assignment
|
||||
- `MaybeAlias a -> b` - Possible aliasing
|
||||
- `CreateFrom a -> b` - Created from source
|
||||
|
||||
**Mutation Effects:**
|
||||
- `Mutate value` - Value is mutated
|
||||
- `MutateTransitive value` - Value and transitive captures are mutated
|
||||
- `MutateConditionally value` - May mutate
|
||||
- `MutateTransitiveConditionally value` - May mutate transitively
|
||||
|
||||
**Other Effects:**
|
||||
- `Render place` - Place is used in render context (JSX props, component return)
|
||||
- `Freeze place` - Place is frozen (made immutable)
|
||||
- `Create place` - New value created
|
||||
- `CreateFunction` - Function expression created, includes `captures` array
|
||||
- `Apply` - Function application with receiver, function, args, and result
|
||||
|
||||
### Hook Aliasing Signatures
|
||||
|
||||
Located in `src/HIR/Globals.ts`, hooks can define custom aliasing signatures to control how data flows through them.
|
||||
|
||||
**Structure:**
|
||||
```typescript
|
||||
aliasing: {
|
||||
receiver: '@receiver', // The hook function itself
|
||||
params: ['@param0'], // Named positional parameters
|
||||
rest: '@rest', // Rest parameters (or null)
|
||||
returns: '@returns', // Return value
|
||||
temporaries: [], // Temporary values during execution
|
||||
effects: [ // Array of effects to apply when hook is called
|
||||
{kind: 'Freeze', value: '@param0', reason: ValueReason.HookCaptured},
|
||||
{kind: 'Assign', from: '@param0', into: '@returns'},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Common patterns:**
|
||||
|
||||
1. **RenderHookAliasing** (useState, useContext, useMemo, useCallback):
|
||||
- Freezes arguments (`Freeze @rest`)
|
||||
- Marks arguments as render-time (`Render @rest`)
|
||||
- Creates frozen return value
|
||||
- Aliases arguments to return
|
||||
|
||||
2. **EffectHookAliasing** (useEffect, useLayoutEffect, useInsertionEffect):
|
||||
- Freezes function and deps
|
||||
- Creates internal effect object
|
||||
- Captures function and deps into effect
|
||||
- Returns undefined
|
||||
|
||||
3. **Event handler hooks** (useEffectEvent):
|
||||
- Freezes callback (`Freeze @fn`)
|
||||
- Aliases input to return (`Assign @fn -> @returns`)
|
||||
- NO Render effect (callback not called during render)
|
||||
|
||||
**Example: useEffectEvent**
|
||||
```typescript
|
||||
const UseEffectEventHook = addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [Effect.Freeze], // Takes one positional param
|
||||
restParam: null,
|
||||
returnType: {kind: 'Function', ...},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffectEvent',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@fn'], // Name for the callback parameter
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured},
|
||||
{kind: 'Assign', from: '@fn', into: '@returns'},
|
||||
// Note: NO Render effect - callback is not called during render
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectEventId,
|
||||
);
|
||||
|
||||
// Add as both names for compatibility
|
||||
['useEffectEvent', UseEffectEventHook],
|
||||
['experimental_useEffectEvent', UseEffectEventHook],
|
||||
```
|
||||
|
||||
**Key insight:** If a hook is missing an `aliasing` config, it falls back to `DefaultNonmutatingHook` which includes a `Render` effect on all arguments. This can cause false positives for hooks like `useEffectEvent` whose callbacks are not called during render.
|
||||
|
||||
## Feature Flags
|
||||
|
||||
Feature flags are configured in `src/HIR/Environment.ts`, for example `enableJsxOutlining`. Test fixtures can override the active feature flags used for that fixture via a comment pragma on the first line of the fixture input, for example:
|
||||
|
||||
```javascript
|
||||
// enableJsxOutlining @enableChangeVariableCodegen:false
|
||||
|
||||
...code...
|
||||
```
|
||||
|
||||
Would enable the `enableJsxOutlining` feature and disable the `enableChangeVariableCodegen` feature.
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. Run `yarn snap -p <fixture>` to see full HIR output with effects
|
||||
2. Look for `@aliasingEffects=` on FunctionExpressions
|
||||
3. Look for `Impure`, `Render`, `Capture` effects on instructions
|
||||
4. Check the pass ordering in Pipeline.ts to understand when effects are populated vs validated
|
||||
|
||||
## Error Handling for Unsupported Features
|
||||
|
||||
When the compiler encounters an unsupported but known pattern, use `CompilerError.throwTodo()` instead of `CompilerError.invariant()`. Todo errors cause graceful bailouts in production; Invariant errors are hard failures indicating unexpected/invalid states.
|
||||
|
||||
```typescript
|
||||
// Unsupported but expected pattern - graceful bailout
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support [description of unsupported feature]`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
|
||||
// Invariant is for truly unexpected/invalid states - hard failure
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected [thing]`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
```
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
@@ -22,7 +22,7 @@ class Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
'use todo memo';
|
||||
return <div>hello world!</div>;
|
||||
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
"use todo memo";
|
||||
return <div>hello world!</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @customOptOutDirectives:["use todo memo"]
|
||||
// @expectNothingCompiled @customOptOutDirectives:["use todo memo"]
|
||||
function Component() {
|
||||
'use todo memo';
|
||||
return <div>hello world!</div>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -37,7 +37,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import { useMemo } from "react";
|
||||
import { makeObject_Primitives, ValidateMemoization } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
// @expectNothingCompiled @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees
|
||||
import {useMemo} from 'react';
|
||||
import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @gating
|
||||
// @expectNothingCompiled @gating
|
||||
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
|
||||
|
||||
export default 42;
|
||||
@@ -12,7 +12,7 @@ export default 42;
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @gating
|
||||
// @expectNothingCompiled @gating
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
|
||||
export default 42;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @gating
|
||||
// @expectNothingCompiled @gating
|
||||
import {isForgetEnabled_Fixtures} from 'ReactForgetFeatureFlag';
|
||||
|
||||
export default 42;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Takes multiple parameters - not a component!
|
||||
function Component(foo, bar) {
|
||||
return <div />;
|
||||
@@ -18,7 +18,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Takes multiple parameters - not a component!
|
||||
function Component(foo, bar) {
|
||||
return <div />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Takes multiple parameters - not a component!
|
||||
function Component(foo, bar) {
|
||||
return <div />;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
import {useIdentity, identity} from 'shared-runtime';
|
||||
|
||||
function Component(fakeProps: number) {
|
||||
@@ -20,7 +20,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
import { useIdentity, identity } from "shared-runtime";
|
||||
|
||||
function Component(fakeProps: number) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
import {useIdentity, identity} from 'shared-runtime';
|
||||
|
||||
function Component(fakeProps: number) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const result = f(props);
|
||||
function helper() {
|
||||
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const result = f(props);
|
||||
function helper() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const result = f(props);
|
||||
function helper() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ignore = <foo />;
|
||||
return {foo: f(props)};
|
||||
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ignore = <foo />;
|
||||
return { foo: f(props) };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
function Component(props) {
|
||||
const ignore = <foo />;
|
||||
return {foo: f(props)};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This component is skipped bc it doesn't call any hooks or
|
||||
// use JSX:
|
||||
function Component(props) {
|
||||
@@ -14,7 +14,7 @@ function Component(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This component is skipped bc it doesn't call any hooks or
|
||||
// use JSX:
|
||||
function Component(props) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This component is skipped bc it doesn't call any hooks or
|
||||
// use JSX:
|
||||
function Component(props) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
@@ -20,7 +20,7 @@ function makeListener(instance) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Regression test for some internal code.
|
||||
// This shows how the "callback rule" is more relaxed,
|
||||
// and doesn't kick in unless we're confident we're in
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
@@ -16,7 +16,7 @@ function createHook() {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can call hooks.
|
||||
function createHook() {
|
||||
return function useHook() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
@@ -15,7 +15,7 @@ function createHook() {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because hooks can use hooks.
|
||||
function createHook() {
|
||||
return function useHookWithHook() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
@@ -15,7 +15,7 @@ function createComponentWithHook() {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// Valid because components can use hooks.
|
||||
function createComponentWithHook() {
|
||||
return function ComponentWithHook() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
// Valid because they're not matching use[A-Z].
|
||||
fooState();
|
||||
_use();
|
||||
@@ -15,6 +16,7 @@ jest.useFakeTimer();
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
// Valid because they're not matching use[A-Z].
|
||||
fooState();
|
||||
_use();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @expectNothingCompiled
|
||||
// Valid because they're not matching use[A-Z].
|
||||
fooState();
|
||||
_use();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
@@ -16,6 +17,7 @@ class C {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @expectNothingCompiled
|
||||
// Valid because classes can call functions.
|
||||
// We don't consider these to be hooks.
|
||||
class C {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This is valid because "use"-prefixed functions called in
|
||||
// unnamed function arguments are not assumed to be hooks.
|
||||
unknownFunction(function (foo, bar) {
|
||||
@@ -16,7 +16,7 @@ unknownFunction(function (foo, bar) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This is valid because "use"-prefixed functions called in
|
||||
// unnamed function arguments are not assumed to be hooks.
|
||||
unknownFunction(function (foo, bar) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @expectNothingCompiled @compilationMode:"infer"
|
||||
// This is valid because "use"-prefixed functions called in
|
||||
// unnamed function arguments are not assumed to be hooks.
|
||||
unknownFunction(function (foo, bar) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Invalid because it's dangerous.
|
||||
@@ -22,7 +22,7 @@ useCustomHook();
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Invalid because it's dangerous.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Invalid because it's dangerous.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// This is a false positive (it's valid) that unfortunately
|
||||
@@ -20,7 +20,7 @@ class Foo extends Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// This is a false positive (it's valid) that unfortunately
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// This is a false positive (it's valid) that unfortunately
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Technically this is a false positive.
|
||||
@@ -23,7 +23,7 @@ const browserHistory = useBasename(createHistory)({
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Technically this is a false positive.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
// Technically this is a false positive.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
@@ -16,7 +16,7 @@
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
@@ -16,7 +16,7 @@ class ClassComponentWithHook extends React.Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithHook extends React.Component {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
@@ -18,7 +18,7 @@ class ClassComponentWithFeatureFlag extends React.Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class ClassComponentWithFeatureFlag extends React.Component {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
@@ -16,7 +16,7 @@
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class C {
|
||||
@@ -17,7 +17,7 @@ class C {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class C {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
class C {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
@@ -16,7 +16,7 @@
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
@@ -16,7 +16,7 @@
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @skip
|
||||
// @expectNothingCompiled @skip
|
||||
// Passed but should have failed
|
||||
|
||||
(class {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
import {c as useMemoCache} from 'react/compiler-runtime';
|
||||
|
||||
function Component(props) {
|
||||
@@ -26,6 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
import { c as useMemoCache } from "react/compiler-runtime";
|
||||
|
||||
function Component(props) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @expectNothingCompiled
|
||||
import {c as useMemoCache} from 'react/compiler-runtime';
|
||||
|
||||
function Component(props) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
function Component() {
|
||||
'use no forget';
|
||||
return <div>Hello World</div>;
|
||||
@@ -18,6 +19,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
function Component() {
|
||||
"use no forget";
|
||||
return <div>Hello World</div>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @expectNothingCompiled
|
||||
function Component() {
|
||||
'use no forget';
|
||||
return <div>Hello World</div>;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
function Component(props) {
|
||||
'use no memo';
|
||||
let x = [props.foo];
|
||||
@@ -19,6 +20,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @expectNothingCompiled
|
||||
function Component(props) {
|
||||
"use no memo";
|
||||
let x = [props.foo];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @expectNothingCompiled
|
||||
function Component(props) {
|
||||
'use no memo';
|
||||
let x = [props.foo];
|
||||
|
||||
@@ -52,7 +52,11 @@ function makePluginOptions(
|
||||
EffectEnum: typeof Effect,
|
||||
ValueKindEnum: typeof ValueKind,
|
||||
ValueReasonEnum: typeof ValueReason,
|
||||
): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
|
||||
): {
|
||||
options: PluginOptions;
|
||||
loggerTestOnly: boolean;
|
||||
logs: Array<{filename: string | null; event: LoggerEvent}>;
|
||||
} {
|
||||
// TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
|
||||
let validatePreserveExistingMemoizationGuarantees = false;
|
||||
let target: CompilerReactTarget = '19';
|
||||
@@ -69,13 +73,12 @@ function makePluginOptions(
|
||||
validatePreserveExistingMemoizationGuarantees = true;
|
||||
}
|
||||
|
||||
const loggerTestOnly = firstLine.includes('@loggerTestOnly');
|
||||
const logs: Array<{filename: string | null; event: LoggerEvent}> = [];
|
||||
const logger: Logger = {
|
||||
logEvent: firstLine.includes('@loggerTestOnly')
|
||||
? (filename, event) => {
|
||||
logs.push({filename, event});
|
||||
}
|
||||
: () => {},
|
||||
logEvent: (filename, event) => {
|
||||
logs.push({filename, event});
|
||||
},
|
||||
debugLogIRs: debugIRLogger,
|
||||
};
|
||||
|
||||
@@ -96,7 +99,7 @@ function makePluginOptions(
|
||||
enableReanimatedCheck: false,
|
||||
target,
|
||||
};
|
||||
return [options, logs];
|
||||
return {options, loggerTestOnly, logs};
|
||||
}
|
||||
|
||||
export function parseInput(
|
||||
@@ -245,7 +248,7 @@ export async function transformFixtureInput(
|
||||
/**
|
||||
* Get Forget compiled code
|
||||
*/
|
||||
const [options, logs] = makePluginOptions(
|
||||
const {options, loggerTestOnly, logs} = makePluginOptions(
|
||||
firstLine,
|
||||
parseConfigPragmaFn,
|
||||
debugIRLogger,
|
||||
@@ -342,7 +345,7 @@ export async function transformFixtureInput(
|
||||
}
|
||||
const forgetOutput = await format(forgetCode, language);
|
||||
let formattedLogs = null;
|
||||
if (logs.length !== 0) {
|
||||
if (loggerTestOnly && logs.length !== 0) {
|
||||
formattedLogs = logs
|
||||
.map(({event}) => {
|
||||
return JSON.stringify(event, (key, value) => {
|
||||
@@ -358,6 +361,23 @@ export async function transformFixtureInput(
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
const expectNothingCompiled =
|
||||
firstLine.indexOf('@expectNothingCompiled') !== -1;
|
||||
const successFailures = logs.filter(
|
||||
log =>
|
||||
log.event.kind === 'CompileSuccess' || log.event.kind === 'CompileError',
|
||||
);
|
||||
if (successFailures.length === 0 && !expectNothingCompiled) {
|
||||
return {
|
||||
kind: 'err',
|
||||
msg: 'No success/failure events, add `// @expectNothingCompiled` to the first line if this is expected',
|
||||
};
|
||||
} else if (successFailures.length !== 0 && expectNothingCompiled) {
|
||||
return {
|
||||
kind: 'err',
|
||||
msg: 'Expected nothing to be compiled (from `// @expectNothingCompiled`), but some functions compiled or errored',
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'ok',
|
||||
value: {
|
||||
|
||||
@@ -26,5 +26,3 @@ export const FIXTURES_PATH = path.join(
|
||||
'compiler',
|
||||
);
|
||||
export const SNAPSHOT_EXTENSION = '.expect.md';
|
||||
export const FILTER_FILENAME = 'testfilter.txt';
|
||||
export const FILTER_PATH = path.join(PROJECT_ROOT, FILTER_FILENAME);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import fs from 'fs/promises';
|
||||
import * as glob from 'glob';
|
||||
import path from 'path';
|
||||
import {FILTER_PATH, FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
|
||||
import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
|
||||
|
||||
const INPUT_EXTENSIONS = [
|
||||
'.js',
|
||||
@@ -22,19 +22,9 @@ const INPUT_EXTENSIONS = [
|
||||
];
|
||||
|
||||
export type TestFilter = {
|
||||
debug: boolean;
|
||||
paths: Array<string>;
|
||||
};
|
||||
|
||||
async function exists(file: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stripExtension(filename: string, extensions: Array<string>): string {
|
||||
for (const ext of extensions) {
|
||||
if (filename.endsWith(ext)) {
|
||||
@@ -44,37 +34,6 @@ function stripExtension(filename: string, extensions: Array<string>): string {
|
||||
return filename;
|
||||
}
|
||||
|
||||
export async function readTestFilter(): Promise<TestFilter | null> {
|
||||
if (!(await exists(FILTER_PATH))) {
|
||||
throw new Error(`testfilter file not found at \`${FILTER_PATH}\``);
|
||||
}
|
||||
|
||||
const input = await fs.readFile(FILTER_PATH, 'utf8');
|
||||
const lines = input.trim().split('\n');
|
||||
|
||||
let debug: boolean = false;
|
||||
const line0 = lines[0];
|
||||
if (line0 != null) {
|
||||
// Try to parse pragmas
|
||||
let consumedLine0 = false;
|
||||
if (line0.indexOf('@only') !== -1) {
|
||||
consumedLine0 = true;
|
||||
}
|
||||
if (line0.indexOf('@debug') !== -1) {
|
||||
debug = true;
|
||||
consumedLine0 = true;
|
||||
}
|
||||
|
||||
if (consumedLine0) {
|
||||
lines.shift();
|
||||
}
|
||||
}
|
||||
return {
|
||||
debug,
|
||||
paths: lines.filter(line => !line.trimStart().startsWith('//')),
|
||||
};
|
||||
}
|
||||
|
||||
export function getBasename(fixture: TestFixture): string {
|
||||
return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import watcher from '@parcel/watcher';
|
||||
import path from 'path';
|
||||
import ts from 'typescript';
|
||||
import {FILTER_FILENAME, FIXTURES_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, readTestFilter} from './fixture-utils';
|
||||
import {FIXTURES_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, getFixtures} from './fixture-utils';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
export function watchSrc(
|
||||
@@ -117,6 +117,16 @@ export type RunnerState = {
|
||||
lastUpdate: number;
|
||||
mode: RunnerMode;
|
||||
filter: TestFilter | null;
|
||||
debug: boolean;
|
||||
// Input mode for interactive pattern entry
|
||||
inputMode: 'none' | 'pattern';
|
||||
inputBuffer: string;
|
||||
// Autocomplete state
|
||||
allFixtureNames: Array<string>;
|
||||
matchingFixtures: Array<string>;
|
||||
selectedIndex: number;
|
||||
// Track last run status of each fixture (for autocomplete suggestions)
|
||||
fixtureLastRunStatus: Map<string, 'pass' | 'fail'>;
|
||||
};
|
||||
|
||||
function subscribeFixtures(
|
||||
@@ -142,26 +152,6 @@ function subscribeFixtures(
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeFilterFile(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
) {
|
||||
watcher.subscribe(PROJECT_ROOT, async (err, events) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
} else if (
|
||||
events.findIndex(event => event.path.includes(FILTER_FILENAME)) !== -1
|
||||
) {
|
||||
if (state.mode.filter) {
|
||||
state.filter = await readTestFilter();
|
||||
state.mode.action = RunnerAction.Test;
|
||||
onChange(state);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeTsc(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
@@ -195,20 +185,226 @@ function subscribeTsc(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Levenshtein edit distance between two strings
|
||||
*/
|
||||
function editDistance(a: string, b: string): number {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
|
||||
// Create a 2D array for memoization
|
||||
const dp: number[][] = Array.from({length: m + 1}, () =>
|
||||
Array(n + 1).fill(0),
|
||||
);
|
||||
|
||||
// Base cases
|
||||
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
||||
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
||||
|
||||
// Fill in the rest
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (a[i - 1] === b[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m][n];
|
||||
}
|
||||
|
||||
function filterFixtures(
|
||||
allNames: Array<string>,
|
||||
pattern: string,
|
||||
): Array<string> {
|
||||
if (pattern === '') {
|
||||
return allNames;
|
||||
}
|
||||
const lowerPattern = pattern.toLowerCase();
|
||||
const matches = allNames.filter(name =>
|
||||
name.toLowerCase().includes(lowerPattern),
|
||||
);
|
||||
// Sort by edit distance (lower = better match)
|
||||
matches.sort((a, b) => {
|
||||
const distA = editDistance(lowerPattern, a.toLowerCase());
|
||||
const distB = editDistance(lowerPattern, b.toLowerCase());
|
||||
return distA - distB;
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
|
||||
const MAX_DISPLAY = 15;
|
||||
|
||||
function renderAutocomplete(state: RunnerState): void {
|
||||
// Clear terminal
|
||||
console.log('\u001Bc');
|
||||
|
||||
// Show current input
|
||||
console.log(`Pattern: ${state.inputBuffer}`);
|
||||
console.log('');
|
||||
|
||||
// Get current filter pattern if active
|
||||
const currentFilterPattern =
|
||||
state.mode.filter && state.filter ? state.filter.paths[0] : null;
|
||||
|
||||
// Show matching fixtures (limit to MAX_DISPLAY)
|
||||
const toShow = state.matchingFixtures.slice(0, MAX_DISPLAY);
|
||||
|
||||
toShow.forEach((name, i) => {
|
||||
const isSelected = i === state.selectedIndex;
|
||||
const matchesCurrentFilter =
|
||||
currentFilterPattern != null &&
|
||||
name.toLowerCase().includes(currentFilterPattern.toLowerCase());
|
||||
|
||||
let prefix: string;
|
||||
if (isSelected) {
|
||||
prefix = '> ';
|
||||
} else if (matchesCurrentFilter) {
|
||||
prefix = '* ';
|
||||
} else {
|
||||
prefix = ' ';
|
||||
}
|
||||
console.log(`${prefix}${name}`);
|
||||
});
|
||||
|
||||
if (state.matchingFixtures.length > MAX_DISPLAY) {
|
||||
console.log(
|
||||
` ... and ${state.matchingFixtures.length - MAX_DISPLAY} more`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('↑/↓/Tab navigate | Enter select | Esc cancel');
|
||||
}
|
||||
|
||||
function subscribeKeyEvents(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
) {
|
||||
process.stdin.on('keypress', async (str, key) => {
|
||||
// Handle input mode (pattern entry with autocomplete)
|
||||
if (state.inputMode !== 'none') {
|
||||
if (key.name === 'return') {
|
||||
// Enter pressed - use selected fixture or typed text
|
||||
let pattern: string;
|
||||
if (
|
||||
state.selectedIndex >= 0 &&
|
||||
state.selectedIndex < state.matchingFixtures.length
|
||||
) {
|
||||
pattern = state.matchingFixtures[state.selectedIndex];
|
||||
} else {
|
||||
pattern = state.inputBuffer.trim();
|
||||
}
|
||||
|
||||
state.inputMode = 'none';
|
||||
state.inputBuffer = '';
|
||||
state.allFixtureNames = [];
|
||||
state.matchingFixtures = [];
|
||||
state.selectedIndex = -1;
|
||||
|
||||
if (pattern !== '') {
|
||||
state.filter = {paths: [pattern]};
|
||||
state.mode.filter = true;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
onChange(state);
|
||||
}
|
||||
return;
|
||||
} else if (key.name === 'escape') {
|
||||
// Cancel input mode
|
||||
state.inputMode = 'none';
|
||||
state.inputBuffer = '';
|
||||
state.allFixtureNames = [];
|
||||
state.matchingFixtures = [];
|
||||
state.selectedIndex = -1;
|
||||
// Redraw normal UI
|
||||
onChange(state);
|
||||
return;
|
||||
} else if (key.name === 'up' || (key.name === 'tab' && key.shift)) {
|
||||
// Navigate up in autocomplete list
|
||||
if (state.matchingFixtures.length > 0) {
|
||||
if (state.selectedIndex <= 0) {
|
||||
state.selectedIndex =
|
||||
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
|
||||
} else {
|
||||
state.selectedIndex--;
|
||||
}
|
||||
renderAutocomplete(state);
|
||||
}
|
||||
return;
|
||||
} else if (key.name === 'down' || (key.name === 'tab' && !key.shift)) {
|
||||
// Navigate down in autocomplete list
|
||||
if (state.matchingFixtures.length > 0) {
|
||||
const maxIndex =
|
||||
Math.min(state.matchingFixtures.length, MAX_DISPLAY) - 1;
|
||||
if (state.selectedIndex >= maxIndex) {
|
||||
state.selectedIndex = 0;
|
||||
} else {
|
||||
state.selectedIndex++;
|
||||
}
|
||||
renderAutocomplete(state);
|
||||
}
|
||||
return;
|
||||
} else if (key.name === 'backspace') {
|
||||
if (state.inputBuffer.length > 0) {
|
||||
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
||||
state.matchingFixtures = filterFixtures(
|
||||
state.allFixtureNames,
|
||||
state.inputBuffer,
|
||||
);
|
||||
state.selectedIndex = -1;
|
||||
renderAutocomplete(state);
|
||||
}
|
||||
return;
|
||||
} else if (str && !key.ctrl && !key.meta) {
|
||||
// Regular character - accumulate, filter, and render
|
||||
state.inputBuffer += str;
|
||||
state.matchingFixtures = filterFixtures(
|
||||
state.allFixtureNames,
|
||||
state.inputBuffer,
|
||||
);
|
||||
state.selectedIndex = -1;
|
||||
renderAutocomplete(state);
|
||||
return;
|
||||
}
|
||||
return; // Ignore other keys in input mode
|
||||
}
|
||||
|
||||
// Normal mode keypress handling
|
||||
if (key.name === 'u') {
|
||||
// u => update fixtures
|
||||
state.mode.action = RunnerAction.Update;
|
||||
} else if (key.name === 'q') {
|
||||
process.exit(0);
|
||||
} else if (key.name === 'f') {
|
||||
state.mode.filter = !state.mode.filter;
|
||||
state.filter = state.mode.filter ? await readTestFilter() : null;
|
||||
} else if (key.name === 'a') {
|
||||
// a => exit filter mode and run all tests
|
||||
state.mode.filter = false;
|
||||
state.filter = null;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
} else if (key.name === 'd') {
|
||||
// d => toggle debug logging
|
||||
state.debug = !state.debug;
|
||||
state.mode.action = RunnerAction.Test;
|
||||
} else if (key.name === 'p') {
|
||||
// p => enter pattern input mode with autocomplete
|
||||
state.inputMode = 'pattern';
|
||||
state.inputBuffer = '';
|
||||
|
||||
// Load all fixtures for autocomplete
|
||||
const fixtures = await getFixtures(null);
|
||||
state.allFixtureNames = Array.from(fixtures.keys()).sort();
|
||||
// Show failed fixtures first when no pattern entered
|
||||
const failedFixtures = Array.from(state.fixtureLastRunStatus.entries())
|
||||
.filter(([_, status]) => status === 'fail')
|
||||
.map(([name]) => name)
|
||||
.sort();
|
||||
state.matchingFixtures =
|
||||
failedFixtures.length > 0 ? failedFixtures : state.allFixtureNames;
|
||||
state.selectedIndex = -1;
|
||||
|
||||
renderAutocomplete(state);
|
||||
return; // Don't trigger onChange yet
|
||||
} else {
|
||||
// any other key re-runs tests
|
||||
state.mode.action = RunnerAction.Test;
|
||||
@@ -219,21 +415,37 @@ function subscribeKeyEvents(
|
||||
|
||||
export async function makeWatchRunner(
|
||||
onChange: (state: RunnerState) => void,
|
||||
filterMode: boolean,
|
||||
debugMode: boolean,
|
||||
initialPattern?: string,
|
||||
): Promise<void> {
|
||||
const state = {
|
||||
// Determine initial filter state
|
||||
let filter: TestFilter | null = null;
|
||||
let filterEnabled = false;
|
||||
|
||||
if (initialPattern) {
|
||||
filter = {paths: [initialPattern]};
|
||||
filterEnabled = true;
|
||||
}
|
||||
|
||||
const state: RunnerState = {
|
||||
compilerVersion: 0,
|
||||
isCompilerBuildValid: false,
|
||||
lastUpdate: -1,
|
||||
mode: {
|
||||
action: RunnerAction.Test,
|
||||
filter: filterMode,
|
||||
filter: filterEnabled,
|
||||
},
|
||||
filter: filterMode ? await readTestFilter() : null,
|
||||
filter,
|
||||
debug: debugMode,
|
||||
inputMode: 'none',
|
||||
inputBuffer: '',
|
||||
allFixtureNames: [],
|
||||
matchingFixtures: [],
|
||||
selectedIndex: -1,
|
||||
fixtureLastRunStatus: new Map(),
|
||||
};
|
||||
|
||||
subscribeTsc(state, onChange);
|
||||
subscribeFixtures(state, onChange);
|
||||
subscribeKeyEvents(state, onChange);
|
||||
subscribeFilterFile(state, onChange);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import * as readline from 'readline';
|
||||
import ts from 'typescript';
|
||||
import yargs from 'yargs';
|
||||
import {hideBin} from 'yargs/helpers';
|
||||
import {FILTER_PATH, PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, getFixtures, readTestFilter} from './fixture-utils';
|
||||
import {PROJECT_ROOT} from './constants';
|
||||
import {TestFilter, getFixtures} from './fixture-utils';
|
||||
import {TestResult, TestResults, report, update} from './reporter';
|
||||
import {
|
||||
RunnerAction,
|
||||
@@ -33,9 +33,9 @@ type RunnerOptions = {
|
||||
sync: boolean;
|
||||
workerThreads: boolean;
|
||||
watch: boolean;
|
||||
filter: boolean;
|
||||
update: boolean;
|
||||
pattern?: string;
|
||||
debug: boolean;
|
||||
};
|
||||
|
||||
const opts: RunnerOptions = yargs
|
||||
@@ -59,18 +59,16 @@ const opts: RunnerOptions = yargs
|
||||
.alias('u', 'update')
|
||||
.describe('update', 'Update fixtures')
|
||||
.default('update', false)
|
||||
.boolean('filter')
|
||||
.describe(
|
||||
'filter',
|
||||
'Only run fixtures which match the contents of testfilter.txt',
|
||||
)
|
||||
.default('filter', false)
|
||||
.string('pattern')
|
||||
.alias('p', 'pattern')
|
||||
.describe(
|
||||
'pattern',
|
||||
'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
|
||||
)
|
||||
.boolean('debug')
|
||||
.alias('d', 'debug')
|
||||
.describe('debug', 'Enable debug logging to print HIR for each pass')
|
||||
.default('debug', false)
|
||||
.help('help')
|
||||
.strict()
|
||||
.parseSync(hideBin(process.argv)) as RunnerOptions;
|
||||
@@ -82,12 +80,15 @@ async function runFixtures(
|
||||
worker: Worker & typeof runnerWorker,
|
||||
filter: TestFilter | null,
|
||||
compilerVersion: number,
|
||||
debug: boolean,
|
||||
requireSingleFixture: boolean,
|
||||
): Promise<TestResults> {
|
||||
// We could in theory be fancy about tracking the contents of the fixtures
|
||||
// directory via our file subscription, but it's simpler to just re-read
|
||||
// the directory each time.
|
||||
const fixtures = await getFixtures(filter);
|
||||
const isOnlyFixture = filter !== null && fixtures.size === 1;
|
||||
const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
|
||||
|
||||
let entries: Array<[string, TestResult]>;
|
||||
if (!opts.sync) {
|
||||
@@ -96,12 +97,7 @@ async function runFixtures(
|
||||
for (const [fixtureName, fixture] of fixtures) {
|
||||
work.push(
|
||||
worker
|
||||
.transformFixture(
|
||||
fixture,
|
||||
compilerVersion,
|
||||
(filter?.debug ?? false) && isOnlyFixture,
|
||||
true,
|
||||
)
|
||||
.transformFixture(fixture, compilerVersion, shouldLog, true)
|
||||
.then(result => [fixtureName, result]),
|
||||
);
|
||||
}
|
||||
@@ -113,7 +109,7 @@ async function runFixtures(
|
||||
let output = await runnerWorker.transformFixture(
|
||||
fixture,
|
||||
compilerVersion,
|
||||
(filter?.debug ?? false) && isOnlyFixture,
|
||||
shouldLog,
|
||||
true,
|
||||
);
|
||||
entries.push([fixtureName, output]);
|
||||
@@ -128,7 +124,7 @@ async function onChange(
|
||||
worker: Worker & typeof runnerWorker,
|
||||
state: RunnerState,
|
||||
) {
|
||||
const {compilerVersion, isCompilerBuildValid, mode, filter} = state;
|
||||
const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
|
||||
if (isCompilerBuildValid) {
|
||||
const start = performance.now();
|
||||
|
||||
@@ -142,8 +138,18 @@ async function onChange(
|
||||
worker,
|
||||
mode.filter ? filter : null,
|
||||
compilerVersion,
|
||||
debug,
|
||||
true, // requireSingleFixture in watch mode
|
||||
);
|
||||
const end = performance.now();
|
||||
|
||||
// Track fixture status for autocomplete suggestions
|
||||
for (const [basename, result] of results) {
|
||||
const failed =
|
||||
result.actual !== result.expected || result.unexpectedError != null;
|
||||
state.fixtureLastRunStatus.set(basename, failed ? 'fail' : 'pass');
|
||||
}
|
||||
|
||||
if (mode.action === RunnerAction.Update) {
|
||||
update(results);
|
||||
state.lastUpdate = end;
|
||||
@@ -159,11 +165,13 @@ async function onChange(
|
||||
console.log(
|
||||
'\n' +
|
||||
(mode.filter
|
||||
? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
|
||||
? `Current mode = FILTER, pattern = "${filter?.paths[0] ?? ''}".`
|
||||
: 'Current mode = NORMAL, run all test fixtures.') +
|
||||
'\nWaiting for input or file changes...\n' +
|
||||
'u - update all fixtures\n' +
|
||||
`f - toggle (turn ${mode.filter ? 'off' : 'on'}) filter mode\n` +
|
||||
`d - toggle (turn ${debug ? 'off' : 'on'}) debug logging\n` +
|
||||
'p - enter pattern to filter fixtures\n' +
|
||||
(mode.filter ? 'a - run all tests (exit filter mode)\n' : '') +
|
||||
'q - quit\n' +
|
||||
'[any] - rerun tests\n',
|
||||
);
|
||||
@@ -180,15 +188,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
worker.getStderr().pipe(process.stderr);
|
||||
worker.getStdout().pipe(process.stdout);
|
||||
|
||||
// If pattern is provided, force watch mode off and use pattern filter
|
||||
const shouldWatch = opts.watch && opts.pattern == null;
|
||||
if (opts.watch && opts.pattern != null) {
|
||||
console.warn('NOTE: --watch is ignored when a --pattern is supplied');
|
||||
}
|
||||
// Check if watch mode should be enabled
|
||||
const shouldWatch = opts.watch;
|
||||
|
||||
if (shouldWatch) {
|
||||
makeWatchRunner(state => onChange(worker, state), opts.filter);
|
||||
if (opts.filter) {
|
||||
makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
|
||||
if (opts.pattern) {
|
||||
/**
|
||||
* Warm up wormers when in watch mode. Loading the Forget babel plugin
|
||||
* and all of its transitive dependencies takes 1-3s (per worker) on a M1.
|
||||
@@ -236,14 +241,17 @@ export async function main(opts: RunnerOptions): Promise<void> {
|
||||
let testFilter: TestFilter | null = null;
|
||||
if (opts.pattern) {
|
||||
testFilter = {
|
||||
debug: true,
|
||||
paths: [opts.pattern],
|
||||
};
|
||||
} else if (opts.filter) {
|
||||
testFilter = await readTestFilter();
|
||||
}
|
||||
|
||||
const results = await runFixtures(worker, testFilter, 0);
|
||||
const results = await runFixtures(
|
||||
worker,
|
||||
testFilter,
|
||||
0,
|
||||
opts.debug,
|
||||
false, // no requireSingleFixture in non-watch mode
|
||||
);
|
||||
if (opts.update) {
|
||||
update(results);
|
||||
isSuccess = true;
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
.roboto-font {
|
||||
font-family: "Roboto", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
"wdth" 100;
|
||||
}
|
||||
|
||||
.swipe-recognizer {
|
||||
width: 300px;
|
||||
background: #eee;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
Activity,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useInsertionEffect,
|
||||
useState,
|
||||
useId,
|
||||
useOptimistic,
|
||||
@@ -41,6 +42,26 @@ const b = (
|
||||
);
|
||||
|
||||
function Component() {
|
||||
// Test inserting fonts with style tags using useInsertionEffect. This is not recommended but
|
||||
// used to test that gestures etc works with useInsertionEffect so that stylesheet based
|
||||
// libraries can be properly supported.
|
||||
useInsertionEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.roboto-font {
|
||||
font-family: "Roboto", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-variation-settings:
|
||||
"wdth" 100;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<ViewTransition
|
||||
default={
|
||||
@@ -82,8 +103,59 @@ export default function Page({url, navigate}) {
|
||||
{rotate: '0deg', transformOrigin: '30px 8px'},
|
||||
{rotate: '360deg', transformOrigin: '30px 8px'},
|
||||
];
|
||||
viewTransition.old.animate(keyframes, 250);
|
||||
viewTransition.new.animate(keyframes, 250);
|
||||
const animation1 = viewTransition.old.animate(keyframes, 250);
|
||||
const animation2 = viewTransition.new.animate(keyframes, 250);
|
||||
return () => {
|
||||
animation1.cancel();
|
||||
animation2.cancel();
|
||||
};
|
||||
}
|
||||
|
||||
function onGestureTransition(
|
||||
timeline,
|
||||
{rangeStart, rangeEnd},
|
||||
viewTransition,
|
||||
types
|
||||
) {
|
||||
const keyframes = [
|
||||
{rotate: '0deg', transformOrigin: '30px 8px'},
|
||||
{rotate: '360deg', transformOrigin: '30px 8px'},
|
||||
];
|
||||
const reverse = rangeStart > rangeEnd;
|
||||
if (timeline instanceof AnimationTimeline) {
|
||||
// Native Timeline
|
||||
const options = {
|
||||
timeline: timeline,
|
||||
direction: reverse ? 'normal' : 'reverse',
|
||||
rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
|
||||
rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
|
||||
};
|
||||
const animation1 = viewTransition.old.animate(keyframes, options);
|
||||
const animation2 = viewTransition.new.animate(keyframes, options);
|
||||
return () => {
|
||||
animation1.cancel();
|
||||
animation2.cancel();
|
||||
};
|
||||
} else {
|
||||
// Custom Timeline
|
||||
const options = {
|
||||
direction: reverse ? 'normal' : 'reverse',
|
||||
// We set the delay and duration to represent the span of the range.
|
||||
delay: reverse ? rangeEnd : rangeStart,
|
||||
duration: reverse ? rangeStart - rangeEnd : rangeEnd - rangeStart,
|
||||
};
|
||||
const animation1 = viewTransition.old.animate(keyframes, options);
|
||||
const animation2 = viewTransition.new.animate(keyframes, options);
|
||||
// Let the custom timeline take control of driving the animations.
|
||||
const cleanup1 = timeline.animate(animation1);
|
||||
const cleanup2 = timeline.animate(animation2);
|
||||
return () => {
|
||||
animation1.cancel();
|
||||
animation2.cancel();
|
||||
cleanup1();
|
||||
cleanup2();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function swipeAction() {
|
||||
@@ -131,7 +203,10 @@ export default function Page({url, navigate}) {
|
||||
);
|
||||
|
||||
const exclamation = (
|
||||
<ViewTransition name="exclamation" onShare={onTransition}>
|
||||
<ViewTransition
|
||||
name="exclamation"
|
||||
onShare={onTransition}
|
||||
onGestureShare={onGestureTransition}>
|
||||
<span>
|
||||
<div>!</div>
|
||||
</span>
|
||||
@@ -171,17 +246,20 @@ export default function Page({url, navigate}) {
|
||||
}}>
|
||||
<h1>{!show ? 'A' + counter : 'B'}</h1>
|
||||
</ViewTransition>
|
||||
{show ? (
|
||||
<div>
|
||||
{a}
|
||||
{b}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{b}
|
||||
{a}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
// Using url instead of renderedUrl here lets us only update this on commit.
|
||||
url === '/?b' ? (
|
||||
<div>
|
||||
{a}
|
||||
{b}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{b}
|
||||
{a}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ViewTransition>
|
||||
{show ? (
|
||||
<div>hello{exclamation}</div>
|
||||
|
||||
@@ -114,16 +114,17 @@ export default function SwipeRecognizer({
|
||||
);
|
||||
}
|
||||
function onGestureEnd(changed) {
|
||||
// Reset scroll
|
||||
if (changed) {
|
||||
// Trigger side-effects
|
||||
startTransition(action);
|
||||
}
|
||||
// We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit
|
||||
// before scheduling new updates.
|
||||
if (activeGesture.current !== null) {
|
||||
const cancelGesture = activeGesture.current;
|
||||
activeGesture.current = null;
|
||||
cancelGesture();
|
||||
}
|
||||
if (changed) {
|
||||
// Trigger side-effects
|
||||
startTransition(action);
|
||||
}
|
||||
}
|
||||
function onScrollEnd() {
|
||||
if (touchTimeline.current) {
|
||||
|
||||
16
flow-typed/environments/bom.js
vendored
16
flow-typed/environments/bom.js
vendored
@@ -826,7 +826,7 @@ declare class WebSocket extends EventTarget {
|
||||
bufferedAmount: number;
|
||||
extensions: string;
|
||||
onopen: (ev: any) => mixed;
|
||||
onmessage: (ev: MessageEvent) => mixed;
|
||||
onmessage: (ev: MessageEvent<>) => mixed;
|
||||
onclose: (ev: CloseEvent) => mixed;
|
||||
onerror: (ev: any) => mixed;
|
||||
binaryType: 'blob' | 'arraybuffer';
|
||||
@@ -855,8 +855,8 @@ declare class Worker extends EventTarget {
|
||||
workerOptions?: WorkerOptions
|
||||
): void;
|
||||
onerror: null | ((ev: any) => mixed);
|
||||
onmessage: null | ((ev: MessageEvent) => mixed);
|
||||
onmessageerror: null | ((ev: MessageEvent) => mixed);
|
||||
onmessage: null | ((ev: MessageEvent<>) => mixed);
|
||||
onmessageerror: null | ((ev: MessageEvent<>) => mixed);
|
||||
postMessage(message: any, ports?: any): void;
|
||||
terminate(): void;
|
||||
}
|
||||
@@ -888,14 +888,14 @@ declare class WorkerGlobalScope extends EventTarget {
|
||||
}
|
||||
|
||||
declare class DedicatedWorkerGlobalScope extends WorkerGlobalScope {
|
||||
onmessage: (ev: MessageEvent) => mixed;
|
||||
onmessageerror: (ev: MessageEvent) => mixed;
|
||||
onmessage: (ev: MessageEvent<>) => mixed;
|
||||
onmessageerror: (ev: MessageEvent<>) => mixed;
|
||||
postMessage(message: any, transfer?: Iterable<any>): void;
|
||||
}
|
||||
|
||||
declare class SharedWorkerGlobalScope extends WorkerGlobalScope {
|
||||
name: string;
|
||||
onconnect: (ev: MessageEvent) => mixed;
|
||||
onconnect: (ev: MessageEvent<>) => mixed;
|
||||
}
|
||||
|
||||
declare class WorkerLocation {
|
||||
@@ -2056,8 +2056,8 @@ declare class MessagePort extends EventTarget {
|
||||
start(): void;
|
||||
close(): void;
|
||||
|
||||
onmessage: null | ((ev: MessageEvent) => mixed);
|
||||
onmessageerror: null | ((ev: MessageEvent) => mixed);
|
||||
onmessage: null | ((ev: MessageEvent<>) => mixed);
|
||||
onmessageerror: null | ((ev: MessageEvent<>) => mixed);
|
||||
}
|
||||
|
||||
declare class MessageChannel {
|
||||
|
||||
6
flow-typed/environments/dom.js
vendored
6
flow-typed/environments/dom.js
vendored
@@ -151,7 +151,7 @@ type TransitionEventHandler = (event: TransitionEvent) => mixed;
|
||||
type TransitionEventListener =
|
||||
| {handleEvent: TransitionEventHandler, ...}
|
||||
| TransitionEventHandler;
|
||||
type MessageEventHandler = (event: MessageEvent) => mixed;
|
||||
type MessageEventHandler = (event: MessageEvent<>) => mixed;
|
||||
type MessageEventListener =
|
||||
| {handleEvent: MessageEventHandler, ...}
|
||||
| MessageEventHandler;
|
||||
@@ -845,8 +845,8 @@ declare class PageTransitionEvent extends Event {
|
||||
// https://www.w3.org/TR/2008/WD-html5-20080610/comms.html
|
||||
// and
|
||||
// https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces
|
||||
declare class MessageEvent extends Event {
|
||||
data: mixed;
|
||||
declare class MessageEvent<Data = mixed> extends Event {
|
||||
data: Data;
|
||||
origin: string;
|
||||
lastEventId: string;
|
||||
source: WindowProxy;
|
||||
|
||||
4
flow-typed/environments/html.js
vendored
4
flow-typed/environments/html.js
vendored
@@ -109,8 +109,8 @@ declare class ErrorEvent extends Event {
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
|
||||
declare class BroadcastChannel extends EventTarget {
|
||||
name: string;
|
||||
onmessage: ?(event: MessageEvent) => void;
|
||||
onmessageerror: ?(event: MessageEvent) => void;
|
||||
onmessage: ?(event: MessageEvent<>) => void;
|
||||
onmessageerror: ?(event: MessageEvent<>) => void;
|
||||
|
||||
constructor(name: string): void;
|
||||
postMessage(msg: mixed): void;
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"jest-cli": "^29.4.2",
|
||||
"jest-diff": "^29.4.2",
|
||||
"jest-environment-jsdom": "^29.4.2",
|
||||
"jest-silent-reporter": "^0.6.0",
|
||||
"jest-snapshot-serializer-raw": "^1.2.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "^1.2.3",
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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 {RuleTester} from 'eslint';
|
||||
import {allRules} from '../src/shared/ReactCompiler';
|
||||
|
||||
const ESLintTesterV8 = require('eslint-v8').RuleTester;
|
||||
|
||||
/**
|
||||
* A string template tag that removes padding from the left side of multi-line strings
|
||||
* @param {Array} strings array of code strings (only one expected)
|
||||
*/
|
||||
function normalizeIndent(strings: TemplateStringsArray): string {
|
||||
const codeLines = strings[0]?.split('\n') ?? [];
|
||||
const leftPadding = codeLines[1]?.match(/\s+/)![0] ?? '';
|
||||
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
|
||||
}
|
||||
|
||||
type CompilerTestCases = {
|
||||
valid: RuleTester.ValidTestCase[];
|
||||
invalid: RuleTester.InvalidTestCase[];
|
||||
};
|
||||
|
||||
const tests: CompilerTestCases = {
|
||||
valid: [
|
||||
// ===========================================
|
||||
// Tests for mayContainReactCode heuristic with Flow syntax
|
||||
// Files that should be SKIPPED (no React-like function names)
|
||||
// These contain code that WOULD trigger errors if compiled,
|
||||
// but since the heuristic skips them, no errors are reported.
|
||||
// ===========================================
|
||||
{
|
||||
name: '[Heuristic/Flow] Skips files with only lowercase utility functions',
|
||||
filename: 'utils.js',
|
||||
code: normalizeIndent`
|
||||
function helper(obj) {
|
||||
obj.key = 'value';
|
||||
return obj;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '[Heuristic/Flow] Skips lowercase arrow functions even with mutations',
|
||||
filename: 'helpers.js',
|
||||
code: normalizeIndent`
|
||||
const processData = (input) => {
|
||||
input.modified = true;
|
||||
return input;
|
||||
};
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// ===========================================
|
||||
// Tests for mayContainReactCode heuristic with Flow component/hook syntax
|
||||
// These use Flow's component/hook declarations which should be detected
|
||||
// ===========================================
|
||||
{
|
||||
name: '[Heuristic/Flow] Compiles Flow component declaration - detects prop mutation',
|
||||
filename: 'component.js',
|
||||
code: normalizeIndent`
|
||||
component MyComponent(a: {key: string}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic/Flow] Compiles exported Flow component declaration - detects prop mutation',
|
||||
filename: 'component.js',
|
||||
code: normalizeIndent`
|
||||
export component MyComponent(a: {key: string}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic/Flow] Compiles default exported Flow component declaration - detects prop mutation',
|
||||
filename: 'component.js',
|
||||
code: normalizeIndent`
|
||||
export default component MyComponent(a: {key: string}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic/Flow] Compiles Flow hook declaration - detects argument mutation',
|
||||
filename: 'hooks.js',
|
||||
code: normalizeIndent`
|
||||
hook useMyHook(a: {key: string}) {
|
||||
a.key = 'value';
|
||||
return a;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props or hook arguments/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic/Flow] Compiles exported Flow hook declaration - detects argument mutation',
|
||||
filename: 'hooks.js',
|
||||
code: normalizeIndent`
|
||||
export hook useMyHook(a: {key: string}) {
|
||||
a.key = 'value';
|
||||
return a;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props or hook arguments/,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const eslintTester = new ESLintTesterV8({
|
||||
parser: require.resolve('hermes-eslint'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
},
|
||||
});
|
||||
eslintTester.run('react-compiler', allRules['immutability'].rule, tests);
|
||||
@@ -46,6 +46,35 @@ const tests: CompilerTestCases = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
// ===========================================
|
||||
// Tests for mayContainReactCode heuristic
|
||||
// Files that should be SKIPPED (no React-like function names)
|
||||
// These contain code that WOULD trigger errors if compiled,
|
||||
// but since the heuristic skips them, no errors are reported.
|
||||
// ===========================================
|
||||
{
|
||||
name: '[Heuristic] Skips files with only lowercase utility functions',
|
||||
filename: 'utils.ts',
|
||||
// This mutates an argument, which would be flagged in a component/hook,
|
||||
// but this file is skipped because there are no React-like function names
|
||||
code: normalizeIndent`
|
||||
function helper(obj) {
|
||||
obj.key = 'value';
|
||||
return obj;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Skips lowercase arrow functions even with mutations',
|
||||
filename: 'helpers.ts',
|
||||
// Would be flagged if compiled, but skipped due to lowercase name
|
||||
code: normalizeIndent`
|
||||
const processData = (input) => {
|
||||
input.modified = true;
|
||||
return input;
|
||||
};
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
@@ -68,6 +97,101 @@ const tests: CompilerTestCases = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// ===========================================
|
||||
// Tests for mayContainReactCode heuristic
|
||||
// Files that SHOULD be compiled (have React-like function names)
|
||||
// These contain violations to prove compilation happens.
|
||||
// ===========================================
|
||||
{
|
||||
name: '[Heuristic] Compiles PascalCase function declaration - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
function MyComponent({a}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Compiles PascalCase arrow function - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
const MyComponent = ({a}) => {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Compiles PascalCase function expression - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
const MyComponent = function({a}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Compiles exported function declaration - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
export function MyComponent({a}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Compiles exported arrow function - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
export const MyComponent = ({a}) => {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '[Heuristic] Compiles default exported function - detects prop mutation',
|
||||
filename: 'component.tsx',
|
||||
code: normalizeIndent`
|
||||
export default function MyComponent({a}) {
|
||||
a.key = 'value';
|
||||
return <div />;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: /Modifying component props/,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,8 @@ process.env.NODE_ENV = 'development';
|
||||
module.exports = {
|
||||
setupFiles: [require.resolve('../../scripts/jest/setupEnvironment.js')],
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
moduleNameMapper: {
|
||||
'^babel-plugin-react-compiler$':
|
||||
'<rootDir>/../../compiler/packages/babel-plugin-react-compiler/dist/index.js',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,10 +17,107 @@ import BabelPluginReactCompiler, {
|
||||
LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import type {SourceCode} from 'eslint';
|
||||
import type * as ESTree from 'estree';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import {isDeepStrictEqual} from 'util';
|
||||
import type {ParseResult} from '@babel/parser';
|
||||
|
||||
// Pattern for component names: starts with uppercase letter
|
||||
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
||||
// Pattern for hook names: starts with 'use' followed by uppercase letter or digit
|
||||
const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
|
||||
|
||||
/**
|
||||
* Quick heuristic using ESLint's already-parsed AST to detect if the file
|
||||
* may contain React components or hooks based on function naming patterns.
|
||||
* Only checks top-level declarations since components/hooks are declared at module scope.
|
||||
* Returns true if compilation should proceed, false to skip.
|
||||
*/
|
||||
function mayContainReactCode(sourceCode: SourceCode): boolean {
|
||||
const ast = sourceCode.ast;
|
||||
|
||||
// Only check top-level statements - components/hooks are declared at module scope
|
||||
for (const node of ast.body) {
|
||||
if (checkTopLevelNode(node)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkTopLevelNode(node: ESTree.Node): boolean {
|
||||
// Handle Flow component/hook declarations (hermes-eslint produces these node types)
|
||||
// @ts-expect-error not part of ESTree spec
|
||||
if (node.type === 'ComponentDeclaration' || node.type === 'HookDeclaration') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle: export function MyComponent() {} or export const useHook = () => {}
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
const decl = (node as ESTree.ExportNamedDeclaration).declaration;
|
||||
if (decl != null) {
|
||||
return checkTopLevelNode(decl);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle: export default function MyComponent() {} or export default () => {}
|
||||
if (node.type === 'ExportDefaultDeclaration') {
|
||||
const decl = (node as ESTree.ExportDefaultDeclaration).declaration;
|
||||
// Anonymous default function export - compile conservatively
|
||||
if (
|
||||
decl.type === 'FunctionExpression' ||
|
||||
decl.type === 'ArrowFunctionExpression' ||
|
||||
(decl.type === 'FunctionDeclaration' &&
|
||||
(decl as ESTree.FunctionDeclaration).id == null)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return checkTopLevelNode(decl as ESTree.Node);
|
||||
}
|
||||
|
||||
// Handle: function MyComponent() {}
|
||||
// Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
// Check for Hermes-added flags indicating Flow component/hook syntax
|
||||
if (
|
||||
'__componentDeclaration' in node ||
|
||||
'__hookDeclaration' in node
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const id = (node as ESTree.FunctionDeclaration).id;
|
||||
if (id != null) {
|
||||
const name = id.name;
|
||||
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle: const MyComponent = () => {} or const useHook = function() {}
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
for (const decl of (node as ESTree.VariableDeclaration).declarations) {
|
||||
if (decl.id.type === 'Identifier') {
|
||||
const init = decl.init;
|
||||
if (
|
||||
init != null &&
|
||||
(init.type === 'ArrowFunctionExpression' ||
|
||||
init.type === 'FunctionExpression')
|
||||
) {
|
||||
const name = decl.id.name;
|
||||
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const COMPILER_OPTIONS: PluginOptions = {
|
||||
outputMode: 'lint',
|
||||
panicThreshold: 'none',
|
||||
@@ -216,6 +313,24 @@ export default function runReactCompiler({
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Quick heuristic: skip files that don't appear to contain React code.
|
||||
// We still cache the empty result so subsequent rules don't re-run the check.
|
||||
if (!mayContainReactCode(sourceCode)) {
|
||||
const emptyResult: RunCacheEntry = {
|
||||
sourceCode: sourceCode.text,
|
||||
filename,
|
||||
userOpts,
|
||||
flowSuppressions: [],
|
||||
events: [],
|
||||
};
|
||||
if (entry != null) {
|
||||
Object.assign(entry, emptyResult);
|
||||
} else {
|
||||
cache.push(filename, emptyResult);
|
||||
}
|
||||
return {...emptyResult};
|
||||
}
|
||||
|
||||
const runEntry = runReactCompilerImpl({
|
||||
sourceCode,
|
||||
filename,
|
||||
|
||||
@@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
if (__DEV__) {
|
||||
console.warn('Hello\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello']);
|
||||
assertConsoleWarnDev(['Hello\n in div']);
|
||||
});
|
||||
|
||||
it('passes if all warnings contain a stack', () => {
|
||||
@@ -888,7 +888,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails if act is called without assertConsoleWarnDev', async () => {
|
||||
@@ -1075,7 +1079,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi \n in div');
|
||||
console.warn('Wow \n in div');
|
||||
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
@@ -1085,9 +1093,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hi
|
||||
- Wow
|
||||
- Bye
|
||||
- Hi in div
|
||||
- Wow in div
|
||||
- Bye in div
|
||||
+ Hi in div (at **)
|
||||
+ Wow in div (at **)"
|
||||
`);
|
||||
@@ -1188,16 +1196,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello
|
||||
+ Good day in div (at **)
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1207,16 +1225,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye\n in div');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello in div (at **)
|
||||
+ Good day
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1226,41 +1254,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
`);
|
||||
});
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if all warnings do not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hello');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello in div (at **)
|
||||
+ Good day in div (at **)
|
||||
+ Bye"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1339,12 +1352,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1361,16 +1375,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1382,9 +1396,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev([
|
||||
'Hello',
|
||||
'Hello\n in div',
|
||||
['Good day', {withoutStack: true}],
|
||||
'Bye',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1490,12 +1504,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1524,16 +1539,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1606,13 +1621,18 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
it('fails if component stack is passed twice', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi %s%s', '\n in div', '\n in div');
|
||||
assertConsoleWarnDev(['Hi']);
|
||||
assertConsoleWarnDev(['Hi \n in div (at **)']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s""
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1621,16 +1641,23 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi %s%s', '\n in div', '\n in div');
|
||||
console.warn('Bye %s%s', '\n in div', '\n in div');
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div (at **)',
|
||||
'Bye \n in div (at **)',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Bye %s%s""
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)
|
||||
Bye in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1646,7 +1673,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -1661,7 +1688,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -1677,7 +1704,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail if waitFor is called before asserting', async () => {
|
||||
@@ -1884,7 +1915,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
if (__DEV__) {
|
||||
console.error('Hello\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello']);
|
||||
assertConsoleErrorDev(['Hello\n in div']);
|
||||
});
|
||||
|
||||
it('passes if all errors contain a stack', () => {
|
||||
@@ -1893,7 +1924,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails if act is called without assertConsoleErrorDev', async () => {
|
||||
@@ -2080,7 +2115,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi \n in div');
|
||||
console.error('Wow \n in div');
|
||||
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
@@ -2090,9 +2129,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hi
|
||||
- Wow
|
||||
- Bye
|
||||
- Hi in div
|
||||
- Wow in div
|
||||
- Bye in div
|
||||
+ Hi in div (at **)
|
||||
+ Wow in div (at **)"
|
||||
`);
|
||||
@@ -2192,101 +2231,6 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
+ TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if only error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
assertConsoleErrorDev(['Hello']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if first error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if last error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if middle error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day');
|
||||
console.error('Bye\n in div');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if all errors do not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
console.error('Good day');
|
||||
console.error('Bye');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('regression: checks entire string, not just the first letter', async () => {
|
||||
@@ -2385,12 +2329,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2407,16 +2352,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -2428,9 +2373,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev([
|
||||
'Hello',
|
||||
'Hello\n in div',
|
||||
['Good day', {withoutStack: true}],
|
||||
'Bye',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2536,12 +2481,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2570,16 +2516,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2678,13 +2624,18 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
it('fails if component stack is passed twice', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi %s%s', '\n in div', '\n in div');
|
||||
assertConsoleErrorDev(['Hi']);
|
||||
assertConsoleErrorDev(['Hi \n in div (at **)']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s""
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2693,16 +2644,23 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi %s%s', '\n in div', '\n in div');
|
||||
console.error('Bye %s%s', '\n in div', '\n in div');
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div (at **)',
|
||||
'Bye \n in div (at **)',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Bye %s%s""
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)
|
||||
Bye in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2711,14 +2669,14 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi \n in div');
|
||||
console.error('Bye \n in div');
|
||||
assertConsoleErrorDev('Hi', 'Bye');
|
||||
assertConsoleErrorDev('Hi \n in div', 'Bye \n in div');
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -2733,7 +2691,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -2749,7 +2707,133 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('in <stack> placeholder', () => {
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is used for a component stack instead of an error stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Warning message\n in div');
|
||||
assertConsoleErrorDev(['Warning message\n in <stack>']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "Warning message
|
||||
in <stack>"
|
||||
Received: "Warning message
|
||||
in div (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is used for multiple component stacks', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('First warning\n in span');
|
||||
console.error('Second warning\n in div');
|
||||
assertConsoleErrorDev([
|
||||
'First warning\n in <stack>',
|
||||
'Second warning\n in <stack>',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "First warning
|
||||
in <stack>"
|
||||
Received: "First warning
|
||||
in span (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)").
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "Second warning
|
||||
in <stack>"
|
||||
Received: "Second warning
|
||||
in div (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows `in <stack>` for actual error stack traces', () => {
|
||||
// This should pass - \n in <stack> is correctly used for an error stack
|
||||
console.error(new Error('Something went wrong'));
|
||||
assertConsoleErrorDev(['Error: Something went wrong\n in <stack>']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if error stack trace is present but \\n in <stack> is not expected', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error(new Error('Something went wrong'));
|
||||
assertConsoleErrorDev(['Error: Something went wrong']);
|
||||
});
|
||||
expect(message).toMatch(`Unexpected error stack trace for:`);
|
||||
expect(message).toMatch(`Error: Something went wrong`);
|
||||
expect(message).toMatch(
|
||||
'If this error should include an error stack trace, add \\n in <stack> to your expected message'
|
||||
);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is expected but no stack is present', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Error: Something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
'Error: Something went wrong\n in <stack>',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing error stack trace for:
|
||||
"Error: Something went wrong"
|
||||
|
||||
The expected message uses \\n in <stack> but the actual error doesn't include an error stack trace.
|
||||
If this error should not have an error stack trace, remove \\n in <stack> from your expected message."
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('[Environment] placeholder', () => {
|
||||
// @gate __DEV__
|
||||
it('expands [Server] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Server \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Server] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('expands [Prerender] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Prerender] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('expands [Cache] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Cache \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Cache] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if waitFor is called before asserting', async () => {
|
||||
|
||||
@@ -168,6 +168,53 @@ function normalizeCodeLocInfo(str) {
|
||||
});
|
||||
}
|
||||
|
||||
// Expands environment placeholders like [Server] into ANSI escape sequences.
|
||||
// This allows test assertions to use a cleaner syntax like "[Server] Error:"
|
||||
// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"
|
||||
function expandEnvironmentPlaceholders(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
// [Environment] -> ANSI escape sequence for environment badge
|
||||
// The format is: reset + inverse + " Environment " + reset
|
||||
return str.replace(
|
||||
/^\[(\w+)] /g,
|
||||
(match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',
|
||||
);
|
||||
}
|
||||
|
||||
// The error stack placeholder that can be used in expected messages
|
||||
const ERROR_STACK_PLACEHOLDER = '\n in <stack>';
|
||||
// A marker used to protect the placeholder during normalization
|
||||
const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';
|
||||
|
||||
// Normalizes expected messages, handling special placeholders
|
||||
function normalizeExpectedMessage(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
// Protect the error stack placeholder from normalization
|
||||
// (normalizeCodeLocInfo would add "(at **)" to it)
|
||||
const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);
|
||||
let result = str;
|
||||
if (hasStackPlaceholder) {
|
||||
result = result.replace(
|
||||
ERROR_STACK_PLACEHOLDER,
|
||||
ERROR_STACK_PLACEHOLDER_MARKER,
|
||||
);
|
||||
}
|
||||
result = normalizeCodeLocInfo(result);
|
||||
result = expandEnvironmentPlaceholders(result);
|
||||
if (hasStackPlaceholder) {
|
||||
// Restore the placeholder (remove the "(at **)" that was added)
|
||||
result = result.replace(
|
||||
ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',
|
||||
ERROR_STACK_PLACEHOLDER,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeComponentStack(entry) {
|
||||
if (
|
||||
typeof entry[0] === 'string' &&
|
||||
@@ -187,6 +234,15 @@ const isLikelyAComponentStack = message =>
|
||||
message.includes('\n in ') ||
|
||||
message.includes('\n at '));
|
||||
|
||||
// Error stack traces start with "*Error:" and contain "at" frames with file paths
|
||||
// Component stacks contain "in ComponentName" patterns
|
||||
// This helps validate that \n in <stack> is used correctly
|
||||
const isLikelyAnErrorStackTrace = message =>
|
||||
typeof message === 'string' &&
|
||||
message.includes('Error:') &&
|
||||
// Has "at" frames typical of error stacks (with file:line:col)
|
||||
/\n\s+at .+\(.*:\d+:\d+\)/.test(message);
|
||||
|
||||
export function createLogAssertion(
|
||||
consoleMethod,
|
||||
matcherName,
|
||||
@@ -236,13 +292,11 @@ export function createLogAssertion(
|
||||
|
||||
const withoutStack = options.withoutStack;
|
||||
|
||||
// Warn about invalid global withoutStack values.
|
||||
if (consoleMethod === 'log' && withoutStack !== undefined) {
|
||||
throwFormattedError(
|
||||
`Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
|
||||
);
|
||||
} else if (withoutStack !== undefined && withoutStack !== true) {
|
||||
// withoutStack can only have a value true.
|
||||
throwFormattedError(
|
||||
`The second argument must be {withoutStack: true}.` +
|
||||
`\n\nInstead received ${JSON.stringify(options)}.`,
|
||||
@@ -256,8 +310,11 @@ export function createLogAssertion(
|
||||
const unexpectedLogs = [];
|
||||
const unexpectedMissingComponentStack = [];
|
||||
const unexpectedIncludingComponentStack = [];
|
||||
const unexpectedMissingErrorStack = [];
|
||||
const unexpectedIncludingErrorStack = [];
|
||||
const logsMismatchingFormat = [];
|
||||
const logsWithExtraComponentStack = [];
|
||||
const stackTracePlaceholderMisuses = [];
|
||||
|
||||
// Loop over all the observed logs to determine:
|
||||
// - Which expected logs are missing
|
||||
@@ -319,11 +376,11 @@ export function createLogAssertion(
|
||||
);
|
||||
}
|
||||
|
||||
expectedMessage = normalizeCodeLocInfo(currentExpectedMessage);
|
||||
expectedMessage = normalizeExpectedMessage(currentExpectedMessage);
|
||||
expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
|
||||
} else if (typeof expectedMessageOrArray === 'string') {
|
||||
// Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
|
||||
expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray);
|
||||
expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);
|
||||
// withoutStack: inherit from global option - simplify when withoutStack is removed.
|
||||
if (consoleMethod === 'log') {
|
||||
expectedWithoutStack = true;
|
||||
} else {
|
||||
@@ -381,19 +438,93 @@ export function createLogAssertion(
|
||||
}
|
||||
|
||||
// Main logic to check if log is expected, with the component stack.
|
||||
if (
|
||||
typeof expectedMessage === 'string' &&
|
||||
(normalizedMessage === expectedMessage ||
|
||||
normalizedMessage.includes(expectedMessage))
|
||||
) {
|
||||
// Check for exact match OR if the message matches with a component stack appended
|
||||
let matchesExpectedMessage = false;
|
||||
let expectsErrorStack = false;
|
||||
const hasErrorStack = isLikelyAnErrorStackTrace(message);
|
||||
|
||||
if (typeof expectedMessage === 'string') {
|
||||
if (normalizedMessage === expectedMessage) {
|
||||
matchesExpectedMessage = true;
|
||||
} else if (expectedMessage.includes('\n in <stack>')) {
|
||||
expectsErrorStack = true;
|
||||
// \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")
|
||||
// NOT for React component stacks (e.g., "\n in ComponentName (at **)").
|
||||
// Validate that the actual message looks like an error stack trace.
|
||||
if (!hasErrorStack) {
|
||||
// The actual message doesn't look like an error stack trace.
|
||||
// This is likely a misuse - someone used \n in <stack> for a component stack.
|
||||
stackTracePlaceholderMisuses.push({
|
||||
expected: expectedMessage,
|
||||
received: normalizedMessage,
|
||||
});
|
||||
}
|
||||
|
||||
const expectedMessageWithoutStack = expectedMessage.replace(
|
||||
'\n in <stack>',
|
||||
'',
|
||||
);
|
||||
if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {
|
||||
// Remove the stack trace
|
||||
const remainder = normalizedMessage.slice(
|
||||
expectedMessageWithoutStack.length,
|
||||
);
|
||||
|
||||
// After normalization, both error stacks and component stacks look like
|
||||
// component stacks (at frames are converted to "in ... (at **)" format).
|
||||
// So we check isLikelyAComponentStack for matching purposes.
|
||||
if (isLikelyAComponentStack(remainder)) {
|
||||
const messageWithoutStack = normalizedMessage.replace(
|
||||
remainder,
|
||||
'',
|
||||
);
|
||||
if (messageWithoutStack === expectedMessageWithoutStack) {
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (remainder === '') {
|
||||
// \n in <stack> was expected but there's no stack at all
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (normalizedMessage === expectedMessageWithoutStack) {
|
||||
// \n in <stack> was expected but actual has no stack at all (exact match without stack)
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (
|
||||
hasErrorStack &&
|
||||
!expectedMessage.includes('\n in <stack>') &&
|
||||
normalizedMessage.startsWith(expectedMessage)
|
||||
) {
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesExpectedMessage) {
|
||||
// withoutStack: Check for unexpected/missing component stacks.
|
||||
// These checks can be simplified when withoutStack is removed.
|
||||
if (isLikelyAComponentStack(normalizedMessage)) {
|
||||
if (expectedWithoutStack === true) {
|
||||
if (expectedWithoutStack === true && !hasErrorStack) {
|
||||
// Only report unexpected component stack if it's not an error stack
|
||||
// (error stacks look like component stacks after normalization)
|
||||
unexpectedIncludingComponentStack.push(normalizedMessage);
|
||||
}
|
||||
} else if (expectedWithoutStack !== true) {
|
||||
} else if (expectedWithoutStack !== true && !expectsErrorStack) {
|
||||
unexpectedMissingComponentStack.push(normalizedMessage);
|
||||
}
|
||||
|
||||
// Check for unexpected/missing error stacks
|
||||
if (hasErrorStack && !expectsErrorStack) {
|
||||
// Error stack is present but \n in <stack> was not in the expected message
|
||||
unexpectedIncludingErrorStack.push(normalizedMessage);
|
||||
} else if (
|
||||
expectsErrorStack &&
|
||||
!hasErrorStack &&
|
||||
!isLikelyAComponentStack(normalizedMessage)
|
||||
) {
|
||||
// \n in <stack> was expected but the actual message doesn't have any stack at all
|
||||
// (if it has a component stack, stackTracePlaceholderMisuses already handles it)
|
||||
unexpectedMissingErrorStack.push(normalizedMessage);
|
||||
}
|
||||
|
||||
// Found expected log, remove it from missing.
|
||||
missingExpectedLogs.splice(0, 1);
|
||||
} else {
|
||||
@@ -422,6 +553,21 @@ export function createLogAssertion(
|
||||
)}`;
|
||||
}
|
||||
|
||||
// Wrong %s formatting is a failure.
|
||||
// This is a common mistake when creating new warnings.
|
||||
if (logsMismatchingFormat.length > 0) {
|
||||
throwFormattedError(
|
||||
logsMismatchingFormat
|
||||
.map(
|
||||
item =>
|
||||
`Received ${item.args.length} arguments for a message with ${
|
||||
item.expectedArgCount
|
||||
} placeholders:\n ${printReceived(item.format)}`,
|
||||
)
|
||||
.join('\n\n'),
|
||||
);
|
||||
}
|
||||
|
||||
// Any unexpected warnings should be treated as a failure.
|
||||
if (unexpectedLogs.length > 0) {
|
||||
throwFormattedError(
|
||||
@@ -466,18 +612,33 @@ export function createLogAssertion(
|
||||
);
|
||||
}
|
||||
|
||||
// Wrong %s formatting is a failure.
|
||||
// This is a common mistake when creating new warnings.
|
||||
if (logsMismatchingFormat.length > 0) {
|
||||
// Any logs that include an error stack trace but \n in <stack> wasn't expected.
|
||||
if (unexpectedIncludingErrorStack.length > 0) {
|
||||
throwFormattedError(
|
||||
logsMismatchingFormat
|
||||
`${unexpectedIncludingErrorStack
|
||||
.map(
|
||||
item =>
|
||||
`Received ${item.args.length} arguments for a message with ${
|
||||
item.expectedArgCount
|
||||
} placeholders:\n ${printReceived(item.format)}`,
|
||||
stack =>
|
||||
`Unexpected error stack trace for:\n ${printReceived(stack)}`,
|
||||
)
|
||||
.join('\n\n'),
|
||||
.join(
|
||||
'\n\n',
|
||||
)}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +
|
||||
`(e.g., "Error: message\\n in <stack>").`,
|
||||
);
|
||||
}
|
||||
|
||||
// Any logs that are missing an error stack trace when \n in <stack> was expected.
|
||||
if (unexpectedMissingErrorStack.length > 0) {
|
||||
throwFormattedError(
|
||||
`${unexpectedMissingErrorStack
|
||||
.map(
|
||||
stack =>
|
||||
`Missing error stack trace for:\n ${printReceived(stack)}`,
|
||||
)
|
||||
.join(
|
||||
'\n\n',
|
||||
)}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +
|
||||
`\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -496,6 +657,25 @@ export function createLogAssertion(
|
||||
.join('\n\n'),
|
||||
);
|
||||
}
|
||||
|
||||
// Using \n in <stack> for component stacks is a misuse.
|
||||
// \n in <stack> should only be used for JavaScript Error stack traces,
|
||||
// not for React component stacks.
|
||||
if (stackTracePlaceholderMisuses.length > 0) {
|
||||
throwFormattedError(
|
||||
`${stackTracePlaceholderMisuses
|
||||
.map(
|
||||
item =>
|
||||
`Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +
|
||||
`stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +
|
||||
`Expected: ${printReceived(item.expected)}\n` +
|
||||
`Received: ${printReceived(item.received)}\n\n` +
|
||||
`If this ${logName()} has a component stack, include the full component stack in your expected message ` +
|
||||
`(e.g., "Warning message\\n in ComponentName (at **)").`,
|
||||
)
|
||||
.join('\n\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +79,18 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
|
||||
status: promise.status,
|
||||
};
|
||||
}
|
||||
} else if ('value' in ioInfo) {
|
||||
// If value exists in ioInfo but is undefined (e.g., WeakRef was GC'd),
|
||||
// ensure we still include it in the normalized output for consistency
|
||||
copy.value = {
|
||||
value: undefined,
|
||||
};
|
||||
} else if (ioInfo.name && ioInfo.name !== 'rsc stream') {
|
||||
// For non-rsc-stream IO that doesn't have a value field, add a default.
|
||||
// This handles the case where the server doesn't send the field when WeakRef is GC'd.
|
||||
copy.value = {
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -549,6 +549,13 @@ export function startGestureTransition() {
|
||||
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export function addViewTransitionFinishedListener(
|
||||
transition: RunningViewTransition,
|
||||
callback: () => void,
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
export function createViewTransitionInstance(
|
||||
|
||||
@@ -1085,7 +1085,6 @@ describe('ReactFlight', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @gate renameElementSymbol
|
||||
it('should emit descriptions of errors in dev', async () => {
|
||||
const ClientErrorBoundary = clientReference(ErrorBoundary);
|
||||
|
||||
@@ -1729,7 +1728,8 @@ describe('ReactFlight', () => {
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <... value={{}}>\n' +
|
||||
' ^^^^\n',
|
||||
' ^^^^\n' +
|
||||
' in (at **)',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3258,7 +3258,7 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render({
|
||||
root: ReactServer.createElement(App),
|
||||
});
|
||||
assertConsoleErrorDev(['Error: err']);
|
||||
assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
|
||||
@@ -467,9 +467,11 @@ function useSyncExternalStore<T>(
|
||||
// useSyncExternalStore() composes multiple hooks internally.
|
||||
// Advance the current hook index the same number of times
|
||||
// so that subsequent hooks have the right memoized state.
|
||||
nextHook(); // SyncExternalStore
|
||||
const hook = nextHook(); // SyncExternalStore
|
||||
nextHook(); // Effect
|
||||
const value = getSnapshot();
|
||||
// Read from hook.memoizedState to get the value that was used during render,
|
||||
// not the current value from getSnapshot() which may have changed.
|
||||
const value = hook !== null ? hook.memoizedState : getSnapshot();
|
||||
hookLog.push({
|
||||
displayName: null,
|
||||
primitive: 'SyncExternalStore',
|
||||
|
||||
@@ -734,7 +734,11 @@ describe('ReactHooksInspection', () => {
|
||||
});
|
||||
const results = normalizeSourceLoc(tree);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]).toMatchInlineSnapshot(`
|
||||
expect(results[0]).toMatchInlineSnapshot(
|
||||
{
|
||||
subHooks: [{value: expect.any(Promise)}],
|
||||
},
|
||||
`
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
@@ -759,12 +763,13 @@ describe('ReactHooksInspection', () => {
|
||||
"isStateEditable": false,
|
||||
"name": "Use",
|
||||
"subHooks": [],
|
||||
"value": Promise {},
|
||||
"value": Any<Promise>,
|
||||
},
|
||||
],
|
||||
"value": undefined,
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('useDebugValue', () => {
|
||||
|
||||
2
packages/react-devtools-core/src/backend.js
vendored
2
packages/react-devtools-core/src/backend.js
vendored
@@ -293,7 +293,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
scheduleRetry();
|
||||
}
|
||||
|
||||
function handleMessage(event: MessageEvent) {
|
||||
function handleMessage(event: MessageEvent<>) {
|
||||
let data;
|
||||
try {
|
||||
if (typeof event.data === 'string') {
|
||||
|
||||
@@ -17,6 +17,14 @@ const contentScriptsToInject = [
|
||||
runAt: 'document_end',
|
||||
world: chrome.scripting.ExecutionWorld.ISOLATED,
|
||||
},
|
||||
{
|
||||
id: '@react-devtools/fallback-eval-context',
|
||||
js: ['build/fallbackEvalContext.js'],
|
||||
matches: ['<all_urls>'],
|
||||
persistAcrossSessions: true,
|
||||
runAt: 'document_start',
|
||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||
},
|
||||
{
|
||||
id: '@react-devtools/hook',
|
||||
js: ['build/installHook.js'],
|
||||
|
||||
@@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'eval-in-inspected-window': {
|
||||
const {
|
||||
payload: {tabId, requestId, scriptId, args},
|
||||
} = message;
|
||||
|
||||
chrome.tabs
|
||||
.sendMessage(tabId, {
|
||||
source: 'devtools-page-eval',
|
||||
payload: {
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response) {
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result: null,
|
||||
error: 'No response from content script',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const {result, error} = response;
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result,
|
||||
error,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result: null,
|
||||
error: error?.message || String(error),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js
vendored
Normal file
35
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 {evalScripts} from '../evalScripts';
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data?.source === 'react-devtools-content-script-eval') {
|
||||
const {scriptId, args, requestId} = event.data.payload;
|
||||
const response = {result: null, error: null};
|
||||
try {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
response.result = evalScripts[scriptId].fn.apply(null, args);
|
||||
} catch (err) {
|
||||
response.error = err.message;
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script-eval-response',
|
||||
payload: {
|
||||
requestId,
|
||||
response,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,38 +1,50 @@
|
||||
/* global chrome */
|
||||
/** @flow */
|
||||
|
||||
// We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN
|
||||
// This is the only purpose of this script - to send persisted settings to installHook.js content script
|
||||
|
||||
async function messageListener(event: MessageEvent) {
|
||||
import type {UnknownMessageEvent} from './messages';
|
||||
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
|
||||
import {postMessage} from './messages';
|
||||
|
||||
async function messageListener(event: UnknownMessageEvent) {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.source === 'react-devtools-hook-installer') {
|
||||
if (event.data.payload.handshake) {
|
||||
const settings = await chrome.storage.local.get();
|
||||
const settings: Partial<DevToolsHookSettings> =
|
||||
await chrome.storage.local.get();
|
||||
// If storage was empty (first installation), define default settings
|
||||
if (typeof settings.appendComponentStack !== 'boolean') {
|
||||
settings.appendComponentStack = true;
|
||||
}
|
||||
if (typeof settings.breakOnConsoleErrors !== 'boolean') {
|
||||
settings.breakOnConsoleErrors = false;
|
||||
}
|
||||
if (typeof settings.showInlineWarningsAndErrors !== 'boolean') {
|
||||
settings.showInlineWarningsAndErrors = true;
|
||||
}
|
||||
if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') {
|
||||
settings.hideConsoleLogsInStrictMode = false;
|
||||
}
|
||||
if (
|
||||
typeof settings.disableSecondConsoleLogDimmingInStrictMode !== 'boolean'
|
||||
) {
|
||||
settings.disableSecondConsoleLogDimmingInStrictMode = false;
|
||||
}
|
||||
const hookSettings: DevToolsHookSettings = {
|
||||
appendComponentStack:
|
||||
typeof settings.appendComponentStack === 'boolean'
|
||||
? settings.appendComponentStack
|
||||
: true,
|
||||
breakOnConsoleErrors:
|
||||
typeof settings.breakOnConsoleErrors === 'boolean'
|
||||
? settings.breakOnConsoleErrors
|
||||
: false,
|
||||
showInlineWarningsAndErrors:
|
||||
typeof settings.showInlineWarningsAndErrors === 'boolean'
|
||||
? settings.showInlineWarningsAndErrors
|
||||
: true,
|
||||
hideConsoleLogsInStrictMode:
|
||||
typeof settings.hideConsoleLogsInStrictMode === 'boolean'
|
||||
? settings.hideConsoleLogsInStrictMode
|
||||
: false,
|
||||
disableSecondConsoleLogDimmingInStrictMode:
|
||||
typeof settings.disableSecondConsoleLogDimmingInStrictMode ===
|
||||
'boolean'
|
||||
? settings.disableSecondConsoleLogDimmingInStrictMode
|
||||
: false,
|
||||
};
|
||||
|
||||
window.postMessage({
|
||||
postMessage({
|
||||
source: 'react-devtools-hook-settings-injector',
|
||||
payload: {settings},
|
||||
payload: {settings: hookSettings},
|
||||
});
|
||||
|
||||
window.removeEventListener('message', messageListener);
|
||||
@@ -41,7 +53,7 @@ async function messageListener(event: MessageEvent) {
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
window.postMessage({
|
||||
postMessage({
|
||||
source: 'react-devtools-hook-settings-injector',
|
||||
payload: {handshake: true},
|
||||
});
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
/** @flow */
|
||||
|
||||
import type {UnknownMessageEvent} from './messages';
|
||||
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import {
|
||||
getIfReloadedAndProfiling,
|
||||
getProfilingSettings,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {postMessage} from './messages';
|
||||
|
||||
let resolveHookSettingsInjection;
|
||||
let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void;
|
||||
|
||||
function messageListener(event: MessageEvent) {
|
||||
function messageListener(event: UnknownMessageEvent) {
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.source === 'react-devtools-hook-settings-injector') {
|
||||
const payload = event.data.payload;
|
||||
// In case handshake message was sent prior to hookSettingsInjector execution
|
||||
// We can't guarantee order
|
||||
if (event.data.payload.handshake) {
|
||||
window.postMessage({
|
||||
if (payload.handshake) {
|
||||
postMessage({
|
||||
source: 'react-devtools-hook-installer',
|
||||
payload: {handshake: true},
|
||||
});
|
||||
} else if (event.data.payload.settings) {
|
||||
} else if (payload.settings) {
|
||||
window.removeEventListener('message', messageListener);
|
||||
resolveHookSettingsInjection(event.data.payload.settings);
|
||||
resolveHookSettingsInjection(payload.settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid double execution
|
||||
if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
|
||||
const hookSettingsPromise = new Promise(resolve => {
|
||||
const hookSettingsPromise = new Promise<DevToolsHookSettings>(resolve => {
|
||||
resolveHookSettingsInjection = resolve;
|
||||
});
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
window.postMessage({
|
||||
postMessage({
|
||||
source: 'react-devtools-hook-installer',
|
||||
payload: {handshake: true},
|
||||
});
|
||||
|
||||
42
packages/react-devtools-extensions/src/contentScripts/messages.js
vendored
Normal file
42
packages/react-devtools-extensions/src/contentScripts/messages.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/** @flow */
|
||||
|
||||
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
export function postMessage(event: UnknownMessageEventData): void {
|
||||
window.postMessage(event);
|
||||
}
|
||||
|
||||
export interface UnknownMessageEvent
|
||||
extends MessageEvent<UnknownMessageEventData> {}
|
||||
|
||||
export type UnknownMessageEventData =
|
||||
| HookSettingsInjectorEventData
|
||||
| HookInstallerEventData;
|
||||
|
||||
export type HookInstallerEventData = {
|
||||
source: 'react-devtools-hook-installer',
|
||||
payload: HookInstallerEventPayload,
|
||||
};
|
||||
|
||||
export type HookInstallerEventPayload = HookInstallerEventPayloadHandshake;
|
||||
|
||||
export type HookInstallerEventPayloadHandshake = {
|
||||
handshake: true,
|
||||
};
|
||||
|
||||
export type HookSettingsInjectorEventData = {
|
||||
source: 'react-devtools-hook-settings-injector',
|
||||
payload: HookSettingsInjectorEventPayload,
|
||||
};
|
||||
|
||||
export type HookSettingsInjectorEventPayload =
|
||||
| HookSettingsInjectorEventPayloadHandshake
|
||||
| HookSettingsInjectorEventPayloadSettings;
|
||||
|
||||
export type HookSettingsInjectorEventPayloadHandshake = {
|
||||
handshake: true,
|
||||
};
|
||||
|
||||
export type HookSettingsInjectorEventPayloadSettings = {
|
||||
settings: DevToolsHookSettings,
|
||||
};
|
||||
@@ -117,3 +117,49 @@ function connectPort() {
|
||||
// $FlowFixMe[incompatible-use]
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
}
|
||||
|
||||
let evalRequestId = 0;
|
||||
const evalRequestCallbacks = new Map<number, Function>();
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
switch (msg?.source) {
|
||||
case 'devtools-page-eval': {
|
||||
const {scriptId, args} = msg.payload;
|
||||
const requestId = evalRequestId++;
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script-eval',
|
||||
payload: {
|
||||
requestId,
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
evalRequestCallbacks.set(requestId, sendResponse);
|
||||
return true; // Indicate we will respond asynchronously
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data?.source === 'react-devtools-content-script-eval-response') {
|
||||
const {requestId, response} = event.data.payload;
|
||||
const callback = evalRequestCallbacks.get(requestId);
|
||||
try {
|
||||
if (!callback)
|
||||
throw new Error(
|
||||
`No eval request callback for id "${requestId}" exists.`,
|
||||
);
|
||||
callback(response);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'React DevTools Content Script eval response error occurred:',
|
||||
e,
|
||||
);
|
||||
} finally {
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
112
packages/react-devtools-extensions/src/evalScripts.js
vendored
Normal file
112
packages/react-devtools-extensions/src/evalScripts.js
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export type EvalScriptIds =
|
||||
| 'checkIfReactPresentInInspectedWindow'
|
||||
| 'reload'
|
||||
| 'setBrowserSelectionFromReact'
|
||||
| 'setReactSelectionFromBrowser'
|
||||
| 'viewAttributeSource'
|
||||
| 'viewElementSource';
|
||||
|
||||
/*
|
||||
.fn for fallback in Content Script context
|
||||
.code for chrome.devtools.inspectedWindow.eval()
|
||||
*/
|
||||
type EvalScriptEntry = {
|
||||
fn: (...args: any[]) => any,
|
||||
code: (...args: any[]) => string,
|
||||
};
|
||||
|
||||
/*
|
||||
Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
|
||||
So some fallback functions are no-op or throw error.
|
||||
*/
|
||||
export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
|
||||
checkIfReactPresentInInspectedWindow: {
|
||||
fn: () =>
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
|
||||
code: () =>
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
|
||||
},
|
||||
reload: {
|
||||
fn: () => window.location.reload(),
|
||||
code: () => 'window.location.reload();',
|
||||
},
|
||||
setBrowserSelectionFromReact: {
|
||||
fn: () => {
|
||||
throw new Error('Not supported in fallback eval context');
|
||||
},
|
||||
code: () =>
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
|
||||
'false',
|
||||
},
|
||||
setReactSelectionFromBrowser: {
|
||||
fn: () => {
|
||||
throw new Error('Not supported in fallback eval context');
|
||||
},
|
||||
code: () =>
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
|
||||
'false',
|
||||
},
|
||||
viewAttributeSource: {
|
||||
fn: ({rendererID, elementID, path}) => {
|
||||
return false; // Not supported in fallback eval context
|
||||
},
|
||||
code: ({rendererID, elementID, path}) =>
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementAttributeByPath(' +
|
||||
JSON.stringify(elementID) +
|
||||
',' +
|
||||
JSON.stringify(path) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
},
|
||||
viewElementSource: {
|
||||
fn: ({rendererID, elementID}) => {
|
||||
return false; // Not supported in fallback eval context
|
||||
},
|
||||
code: ({rendererID, elementID}) =>
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementSourceFunctionById(' +
|
||||
JSON.stringify(elementID) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
/* global chrome */
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
|
||||
export function setBrowserSelectionFromReact() {
|
||||
// This is currently only called on demand when you press "view DOM".
|
||||
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
|
||||
// we could make this happen automatically when you select another component.
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
|
||||
'false',
|
||||
evalInInspectedWindow(
|
||||
'setBrowserSelectionFromReact',
|
||||
[],
|
||||
(didSelectionChange, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
@@ -19,10 +18,9 @@ export function setBrowserSelectionFromReact() {
|
||||
export function setReactSelectionFromBrowser(bridge) {
|
||||
// When the user chooses a different node in the browser Elements tab,
|
||||
// copy it over to the hook object so that we can sync the selection.
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
|
||||
'false',
|
||||
evalInInspectedWindow(
|
||||
'setReactSelectionFromBrowser',
|
||||
[],
|
||||
(didSelectionChange, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
@@ -34,7 +32,7 @@ export function setReactSelectionFromBrowser(bridge) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remember to sync the selection next time we show Components tab.
|
||||
// Remember to sync the selection next time we show inspected element
|
||||
bridge.send('syncSelectionFromBuiltinElementsPanel');
|
||||
}
|
||||
},
|
||||
|
||||
116
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js
vendored
Normal file
116
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {EvalScriptIds} from '../evalScripts';
|
||||
|
||||
import {evalScripts} from '../evalScripts';
|
||||
|
||||
type ExceptionInfo = {
|
||||
code: ?string,
|
||||
description: ?string,
|
||||
isError: boolean,
|
||||
isException: boolean,
|
||||
value: any,
|
||||
};
|
||||
|
||||
const EVAL_TIMEOUT = 1000 * 10;
|
||||
|
||||
let evalRequestId = 0;
|
||||
const evalRequestCallbacks = new Map<
|
||||
number,
|
||||
(value: {result: any, error: any}) => void,
|
||||
>();
|
||||
|
||||
function fallbackEvalInInspectedWindow(
|
||||
scriptId: EvalScriptIds,
|
||||
args: any[],
|
||||
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
|
||||
) {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
const code = evalScripts[scriptId].code.apply(null, args);
|
||||
const tabId = chrome.devtools.inspectedWindow.tabId;
|
||||
const requestId = evalRequestId++;
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'devtools-page',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window',
|
||||
tabId,
|
||||
requestId,
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
});
|
||||
const timeout = setTimeout(() => {
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
if (callback) {
|
||||
callback(null, {
|
||||
code,
|
||||
description:
|
||||
'Timed out while waiting for eval response from the inspected window.',
|
||||
isError: true,
|
||||
isException: false,
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
}, EVAL_TIMEOUT);
|
||||
evalRequestCallbacks.set(requestId, ({result, error}) => {
|
||||
clearTimeout(timeout);
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
if (callback) {
|
||||
if (error) {
|
||||
callback(null, {
|
||||
code,
|
||||
description: undefined,
|
||||
isError: false,
|
||||
isException: true,
|
||||
value: error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback(result, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function evalInInspectedWindow(
|
||||
scriptId: EvalScriptIds,
|
||||
args: any[],
|
||||
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
|
||||
) {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
const code = evalScripts[scriptId].code.apply(null, args);
|
||||
chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
|
||||
if (!exceptionInfo) {
|
||||
callback(result, exceptionInfo);
|
||||
return;
|
||||
}
|
||||
// If an exception (e.g. CSP Blocked) occurred,
|
||||
// fallback to the content script eval context
|
||||
fallbackEvalInInspectedWindow(scriptId, args, callback);
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(({payload, source}) => {
|
||||
if (source === 'react-devtools-background') {
|
||||
switch (payload?.type) {
|
||||
case 'eval-in-inspected-window-response': {
|
||||
const {requestId, result, error} = payload;
|
||||
const callback = evalRequestCallbacks.get(requestId);
|
||||
if (callback) {
|
||||
callback({result, error});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
145
packages/react-devtools-extensions/src/main/index.js
vendored
145
packages/react-devtools-extensions/src/main/index.js
vendored
@@ -1,6 +1,14 @@
|
||||
/* global chrome */
|
||||
/** @flow */
|
||||
|
||||
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
|
||||
import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
|
||||
import type {
|
||||
TabID,
|
||||
ViewElementSource,
|
||||
} from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||
import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import {createElement} from 'react';
|
||||
import {flushSync} from 'react-dom';
|
||||
@@ -32,6 +40,7 @@ import {
|
||||
} from './elementSelection';
|
||||
import {viewAttributeSource} from './sourceSelection';
|
||||
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
import {startReactPolling} from './reactPolling';
|
||||
import {cloneStyleTags} from './cloneStyleTags';
|
||||
import fetchFileWithCaching from './fetchFileWithCaching';
|
||||
@@ -50,9 +59,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
|
||||
function createBridge() {
|
||||
bridge = new Bridge({
|
||||
listen(fn) {
|
||||
const bridgeListener = message => fn(message);
|
||||
const bridgeListener = (message: Message) => fn(message);
|
||||
// Store the reference so that we unsubscribe from the same object.
|
||||
const portOnMessage = port.onMessage;
|
||||
const portOnMessage = ((port: any): ExtensionPort).onMessage;
|
||||
portOnMessage.addListener(bridgeListener);
|
||||
|
||||
lastSubscribedBridgeListener = bridgeListener;
|
||||
@@ -70,7 +79,7 @@ function createBridge() {
|
||||
|
||||
bridge.addListener('reloadAppForProfiling', () => {
|
||||
localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
|
||||
chrome.devtools.inspectedWindow.eval('window.location.reload();');
|
||||
evalInInspectedWindow('reload', [], () => {});
|
||||
});
|
||||
|
||||
bridge.addListener(
|
||||
@@ -175,14 +184,20 @@ function createBridgeAndStore() {
|
||||
// Otherwise, the Store may miss important initial tree op codes.
|
||||
injectBackendManager(chrome.devtools.inspectedWindow.tabId);
|
||||
|
||||
const viewAttributeSourceFunction = (id, path) => {
|
||||
const viewAttributeSourceFunction = (
|
||||
id: Element['id'],
|
||||
path: Array<string | number>,
|
||||
) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
viewAttributeSource(rendererID, id, path);
|
||||
}
|
||||
};
|
||||
|
||||
const viewElementSourceFunction = (source, symbolicatedSource) => {
|
||||
const viewElementSourceFunction: ViewElementSource = (
|
||||
source,
|
||||
symbolicatedSource,
|
||||
) => {
|
||||
const [, sourceURL, line, column] = symbolicatedSource
|
||||
? symbolicatedSource
|
||||
: source;
|
||||
@@ -197,7 +212,7 @@ function createBridgeAndStore() {
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
render = (overrideTab = mostRecentOverrideTab) => {
|
||||
render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
|
||||
mostRecentOverrideTab = overrideTab;
|
||||
|
||||
root.render(
|
||||
@@ -205,6 +220,7 @@ function createBridgeAndStore() {
|
||||
bridge,
|
||||
browserTheme: getBrowserTheme(),
|
||||
componentsPortalContainer,
|
||||
inspectedElementPortalContainer,
|
||||
profilerPortalContainer,
|
||||
editorPortalContainer,
|
||||
currentSelectedSource,
|
||||
@@ -225,7 +241,9 @@ function createBridgeAndStore() {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureInitialHTMLIsCleared(container) {
|
||||
function ensureInitialHTMLIsCleared(
|
||||
container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
|
||||
) {
|
||||
if (container._hasInitialHTMLBeenCleared) {
|
||||
return;
|
||||
}
|
||||
@@ -277,6 +295,52 @@ function createComponentsPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
function createElementsInspectPanel() {
|
||||
if (inspectedElementPortalContainer) {
|
||||
// Panel is created and user opened it at least once
|
||||
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
|
||||
render();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (inspectedElementPane) {
|
||||
// Panel is created, but wasn't opened yet, so no document is present for it
|
||||
return;
|
||||
}
|
||||
|
||||
const elementsPanel = chrome.devtools.panels.elements;
|
||||
if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
|
||||
// Firefox will not pass the window to the onShown listener despite setPage
|
||||
// being called.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=2010549
|
||||
|
||||
// May not be supported in some browsers.
|
||||
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels/ElementsPanel/createSidebarPane#browser_compatibility
|
||||
return;
|
||||
}
|
||||
|
||||
elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
|
||||
inspectedElementPane = createdPane;
|
||||
|
||||
createdPane.setPage('panel.html');
|
||||
createdPane.setHeight('75px');
|
||||
|
||||
createdPane.onShown.addListener(portal => {
|
||||
inspectedElementPortalContainer = portal.container;
|
||||
if (inspectedElementPortalContainer != null && render) {
|
||||
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
|
||||
bridge.send('syncSelectionFromBuiltinElementsPanel');
|
||||
|
||||
render();
|
||||
portal.injectStyles(cloneStyleTags);
|
||||
|
||||
logEvent({event_name: 'selected-inspected-element-pane'});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createProfilerPanel() {
|
||||
if (profilerPortalContainer) {
|
||||
// Panel is created and user opened it at least once
|
||||
@@ -350,13 +414,6 @@ function createSourcesEditorPanel() {
|
||||
logEvent({event_name: 'selected-editor-pane'});
|
||||
}
|
||||
});
|
||||
|
||||
createdPane.onShown.addListener(() => {
|
||||
bridge.emit('extensionEditorPaneShown');
|
||||
});
|
||||
createdPane.onHidden.addListener(() => {
|
||||
bridge.emit('extensionEditorPaneHidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,10 +489,10 @@ function performInTabNavigationCleanup() {
|
||||
// Do not clean mostRecentOverrideTab on purpose, so we remember last opened
|
||||
// React DevTools tab, when user does in-tab navigation
|
||||
|
||||
store = null;
|
||||
bridge = null;
|
||||
render = null;
|
||||
root = null;
|
||||
store = (null: $FlowFixMe);
|
||||
bridge = (null: $FlowFixMe);
|
||||
render = (null: $FlowFixMe);
|
||||
root = (null: $FlowFixMe);
|
||||
}
|
||||
|
||||
function performFullCleanup() {
|
||||
@@ -457,18 +514,18 @@ function performFullCleanup() {
|
||||
componentsPortalContainer = null;
|
||||
profilerPortalContainer = null;
|
||||
suspensePortalContainer = null;
|
||||
root = null;
|
||||
root = (null: $FlowFixMe);
|
||||
|
||||
mostRecentOverrideTab = null;
|
||||
store = null;
|
||||
bridge = null;
|
||||
render = null;
|
||||
store = (null: $FlowFixMe);
|
||||
bridge = (null: $FlowFixMe);
|
||||
render = (null: $FlowFixMe);
|
||||
|
||||
port?.disconnect();
|
||||
port = null;
|
||||
port = (null: $FlowFixMe);
|
||||
}
|
||||
|
||||
function connectExtensionPort() {
|
||||
function connectExtensionPort(): void {
|
||||
if (port) {
|
||||
throw new Error('DevTools port was already connected');
|
||||
}
|
||||
@@ -492,7 +549,7 @@ function connectExtensionPort() {
|
||||
// so, when we call `port.disconnect()` from this script,
|
||||
// this should not trigger this callback and port reconnection
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
port = (null: $FlowFixMe);
|
||||
connectExtensionPort();
|
||||
});
|
||||
}
|
||||
@@ -507,6 +564,7 @@ function mountReactDevTools() {
|
||||
createComponentsPanel();
|
||||
createProfilerPanel();
|
||||
createSourcesEditorPanel();
|
||||
createElementsInspectPanel();
|
||||
// Suspense Tab is created via the hook
|
||||
// TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable
|
||||
}
|
||||
@@ -545,9 +603,9 @@ function mountReactDevToolsWhenReactHasLoaded() {
|
||||
);
|
||||
}
|
||||
|
||||
let bridge = null;
|
||||
let bridge: FrontendBridge = (null: $FlowFixMe);
|
||||
let lastSubscribedBridgeListener = null;
|
||||
let store = null;
|
||||
let store: Store = (null: $FlowFixMe);
|
||||
|
||||
let profilingData = null;
|
||||
|
||||
@@ -555,18 +613,35 @@ let componentsPanel = null;
|
||||
let profilerPanel = null;
|
||||
let suspensePanel = null;
|
||||
let editorPane = null;
|
||||
let inspectedElementPane = null;
|
||||
let componentsPortalContainer = null;
|
||||
let profilerPortalContainer = null;
|
||||
let suspensePortalContainer = null;
|
||||
let editorPortalContainer = null;
|
||||
let inspectedElementPortalContainer = null;
|
||||
|
||||
let mostRecentOverrideTab = null;
|
||||
let render = null;
|
||||
let root = null;
|
||||
let mostRecentOverrideTab: null | TabID = null;
|
||||
let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
|
||||
let root: RootType = (null: $FlowFixMe);
|
||||
|
||||
let currentSelectedSource: null | SourceSelection = null;
|
||||
|
||||
let port = null;
|
||||
type ExtensionEvent = {
|
||||
addListener(callback: (message: Message, port: ExtensionPort) => void): void,
|
||||
removeListener(
|
||||
callback: (message: Message, port: ExtensionPort) => void,
|
||||
): void,
|
||||
};
|
||||
|
||||
/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
|
||||
type ExtensionPort = {
|
||||
onDisconnect: ExtensionEvent,
|
||||
onMessage: ExtensionEvent,
|
||||
postMessage(message: mixed, transferable?: Array<mixed>): void,
|
||||
disconnect(): void,
|
||||
};
|
||||
|
||||
let port: ExtensionPort = (null: $FlowFixMe);
|
||||
|
||||
// In case when multiple navigation events emitted in a short period of time
|
||||
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
|
||||
@@ -599,7 +674,7 @@ connectExtensionPort();
|
||||
|
||||
mountReactDevToolsWhenReactHasLoaded();
|
||||
|
||||
function onThemeChanged(themeName) {
|
||||
function onThemeChanged() {
|
||||
// Rerender with the new theme
|
||||
render();
|
||||
}
|
||||
@@ -636,6 +711,12 @@ if (chrome.devtools.panels.setOpenResourceHandler) {
|
||||
resource.url,
|
||||
lineNumber - 1,
|
||||
columnNumber - 1,
|
||||
maybeError => {
|
||||
if (maybeError && maybeError.isError) {
|
||||
// Not a resource Chrome can open. Fallback to browser default behavior.
|
||||
window.open(resource.url);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global chrome */
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
|
||||
class CouldNotFindReactOnThePageError extends Error {
|
||||
constructor() {
|
||||
@@ -26,8 +26,9 @@ export function startReactPolling(
|
||||
|
||||
// This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
|
||||
function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
|
||||
evalInInspectedWindow(
|
||||
'checkIfReactPresentInInspectedWindow',
|
||||
[],
|
||||
(pageHasReact, exceptionInfo) => {
|
||||
if (status === 'aborted') {
|
||||
onError(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user