Compare commits
54 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 | ||
|
|
5aec1b2a8d | ||
|
|
d6cae440e3 | ||
|
|
00908be9ff | ||
|
|
0e180141bf | ||
|
|
65eec428c4 | ||
|
|
454fc41fc7 | ||
|
|
f93b9fd44b | ||
|
|
b731fe28cc | ||
|
|
88ee1f5955 | ||
|
|
bcf97c7564 | ||
|
|
ba5b843692 |
@@ -593,6 +593,7 @@ module.exports = {
|
||||
mixin$Animatable: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
Partial: 'readonly',
|
||||
PerformanceMeasureOptions: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
@@ -635,6 +636,7 @@ module.exports = {
|
||||
FocusOptions: 'readonly',
|
||||
OptionalEffectTiming: 'readonly',
|
||||
|
||||
__REACT_ROOT_PATH_TEST__: 'readonly',
|
||||
spyOnDev: 'readonly',
|
||||
spyOnDevAndProd: 'readonly',
|
||||
spyOnProd: 'readonly',
|
||||
|
||||
50
.github/workflows/runtime_build_and_test.yml
vendored
50
.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
|
||||
@@ -382,9 +383,6 @@ jobs:
|
||||
-r=experimental --env=development,
|
||||
-r=experimental --env=production,
|
||||
|
||||
# Dev Tools
|
||||
--project=devtools -r=experimental,
|
||||
|
||||
# TODO: Update test config to support www build tests
|
||||
# - "-r=www-classic --env=development --variant=false"
|
||||
# - "-r=www-classic --env=production --variant=false"
|
||||
@@ -448,8 +446,54 @@ 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:
|
||||
name: yarn test-build (devtools)
|
||||
needs: [build_and_lint, runtime_node_modules_cache]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- 1/5
|
||||
- 2/5
|
||||
- 3/5
|
||||
- 4/5
|
||||
- 5/5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache/restore@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Restore archived build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: _build_*
|
||||
path: build
|
||||
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:
|
||||
name: Process artifacts combined
|
||||
needs: [build_and_lint, runtime_node_modules_cache]
|
||||
|
||||
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,
|
||||
});
|
||||
```
|
||||
@@ -225,8 +225,15 @@ export const EnvironmentConfigSchema = z.object({
|
||||
|
||||
/**
|
||||
* Validate that dependencies supplied to effect hooks are exhaustive.
|
||||
* Can be:
|
||||
* - 'off': No validation (default)
|
||||
* - 'all': Validate and report both missing and extra dependencies
|
||||
* - 'missing-only': Only report missing dependencies
|
||||
* - 'extra-only': Only report extra/unnecessary dependencies
|
||||
*/
|
||||
validateExhaustiveEffectDependencies: z.boolean().default(false),
|
||||
validateExhaustiveEffectDependencies: z
|
||||
.enum(['off', 'all', 'missing-only', 'extra-only'])
|
||||
.default('off'),
|
||||
|
||||
/**
|
||||
* When this is true, rather than pruning existing manual memoization but ensuring or validating
|
||||
|
||||
@@ -141,6 +141,7 @@ export function validateExhaustiveDependencies(
|
||||
reactive,
|
||||
startMemo.depsLoc,
|
||||
ErrorCategory.MemoDependencies,
|
||||
'all',
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
@@ -159,7 +160,7 @@ export function validateExhaustiveDependencies(
|
||||
onStartMemoize,
|
||||
onFinishMemoize,
|
||||
onEffect: (inferred, manual, manualMemoLoc) => {
|
||||
if (env.config.validateExhaustiveEffectDependencies === false) {
|
||||
if (env.config.validateExhaustiveEffectDependencies === 'off') {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
@@ -195,12 +196,17 @@ export function validateExhaustiveDependencies(
|
||||
});
|
||||
}
|
||||
}
|
||||
const effectReportMode =
|
||||
typeof env.config.validateExhaustiveEffectDependencies === 'string'
|
||||
? env.config.validateExhaustiveEffectDependencies
|
||||
: 'all';
|
||||
const diagnostic = validateDependencies(
|
||||
Array.from(inferred),
|
||||
manualDeps,
|
||||
reactive,
|
||||
manualMemoLoc,
|
||||
ErrorCategory.EffectExhaustiveDependencies,
|
||||
effectReportMode,
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
@@ -220,6 +226,7 @@ function validateDependencies(
|
||||
category:
|
||||
| ErrorCategory.MemoDependencies
|
||||
| ErrorCategory.EffectExhaustiveDependencies,
|
||||
exhaustiveDepsReportMode: 'all' | 'missing-only' | 'extra-only',
|
||||
): CompilerDiagnostic | null {
|
||||
// Sort dependencies by name and path, with shorter/non-optional paths first
|
||||
inferred.sort((a, b) => {
|
||||
@@ -370,9 +377,20 @@ function validateDependencies(
|
||||
extra.push(dep);
|
||||
}
|
||||
|
||||
if (missing.length !== 0 || extra.length !== 0) {
|
||||
// Filter based on report mode
|
||||
const filteredMissing =
|
||||
exhaustiveDepsReportMode === 'extra-only' ? [] : missing;
|
||||
const filteredExtra =
|
||||
exhaustiveDepsReportMode === 'missing-only' ? [] : extra;
|
||||
|
||||
if (filteredMissing.length !== 0 || filteredExtra.length !== 0) {
|
||||
let suggestion: CompilerSuggestion | null = null;
|
||||
if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') {
|
||||
if (
|
||||
manualMemoLoc != null &&
|
||||
typeof manualMemoLoc !== 'symbol' &&
|
||||
manualMemoLoc.start.index != null &&
|
||||
manualMemoLoc.end.index != null
|
||||
) {
|
||||
suggestion = {
|
||||
description: 'Update dependencies',
|
||||
range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
|
||||
@@ -388,8 +406,13 @@ function validateDependencies(
|
||||
.join(', ')}]`,
|
||||
};
|
||||
}
|
||||
const diagnostic = createDiagnostic(category, missing, extra, suggestion);
|
||||
for (const dep of missing) {
|
||||
const diagnostic = createDiagnostic(
|
||||
category,
|
||||
filteredMissing,
|
||||
filteredExtra,
|
||||
suggestion,
|
||||
);
|
||||
for (const dep of filteredMissing) {
|
||||
let reactiveStableValueHint = '';
|
||||
if (isStableType(dep.identifier)) {
|
||||
reactiveStableValueHint =
|
||||
@@ -402,7 +425,7 @@ function validateDependencies(
|
||||
loc: dep.loc,
|
||||
});
|
||||
}
|
||||
for (const dep of extra) {
|
||||
for (const dep of filteredExtra) {
|
||||
if (dep.root.kind === 'Global') {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
|
||||
@@ -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
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies:"extra-only"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// no error: missing dep not reported in extra-only mode
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// error: extra dep - y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: extra dep - y (missing dep - z not reported)
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: extra dep - x.y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-extra-only.ts:13:9
|
||||
11 | useEffect(() => {
|
||||
12 | log(x);
|
||||
> 13 | }, [x, y]);
|
||||
| ^ Unnecessary dependency `y`
|
||||
14 |
|
||||
15 | // error: extra dep - y (missing dep - z not reported)
|
||||
16 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-extra-only.ts:18:9
|
||||
16 | useEffect(() => {
|
||||
17 | log(x, z);
|
||||
> 18 | }, [x, y]);
|
||||
| ^ Unnecessary dependency `y`
|
||||
19 |
|
||||
20 | // error: extra dep - x.y
|
||||
21 | useEffect(() => {
|
||||
|
||||
Inferred dependencies: `[x, z]`
|
||||
|
||||
Error: Found extra effect dependencies
|
||||
|
||||
Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-extra-only.ts:23:6
|
||||
21 | useEffect(() => {
|
||||
22 | log(x);
|
||||
> 23 | }, [x.y]);
|
||||
| ^^^ Overly precise dependency `x.y`, use `x` instead
|
||||
24 | }
|
||||
25 |
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @validateExhaustiveEffectDependencies:"extra-only"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// no error: missing dep not reported in extra-only mode
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// error: extra dep - y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: extra dep - y (missing dep - z not reported)
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: extra dep - x.y
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies:"missing-only"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// error: missing dep - x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// no error: extra dep not reported in missing-only mode
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep - z (extra dep - y not reported)
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Found missing effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-missing-only.ts:7:8
|
||||
5 | // error: missing dep - x
|
||||
6 | useEffect(() => {
|
||||
> 7 | log(x);
|
||||
| ^ Missing dependency `x`
|
||||
8 | }, []);
|
||||
9 |
|
||||
10 | // no error: extra dep not reported in missing-only mode
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
|
||||
Error: Found missing effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-missing-only.ts:17:11
|
||||
15 | // error: missing dep - z (extra dep - y not reported)
|
||||
16 | useEffect(() => {
|
||||
> 17 | log(x, z);
|
||||
| ^ Missing dependency `z`
|
||||
18 | }, [x, y]);
|
||||
19 |
|
||||
20 | // error: missing dep x
|
||||
|
||||
Inferred dependencies: `[x, z]`
|
||||
|
||||
Error: Found missing effect dependencies
|
||||
|
||||
Missing dependencies can cause an effect to fire less often than it should.
|
||||
|
||||
error.invalid-exhaustive-effect-deps-missing-only.ts:22:8
|
||||
20 | // error: missing dep x
|
||||
21 | useEffect(() => {
|
||||
> 22 | log(x);
|
||||
| ^ Missing dependency `x`
|
||||
23 | }, [x.y]);
|
||||
24 | }
|
||||
25 |
|
||||
|
||||
Inferred dependencies: `[x]`
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @validateExhaustiveEffectDependencies:"missing-only"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
// error: missing dep - x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, []);
|
||||
|
||||
// no error: extra dep not reported in missing-only mode
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep - z (extra dep - y not reported)
|
||||
useEffect(() => {
|
||||
log(x, z);
|
||||
}, [x, y]);
|
||||
|
||||
// error: missing dep x
|
||||
useEffect(() => {
|
||||
log(x);
|
||||
}, [x.y]);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
@@ -69,7 +69,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
|
||||
import {
|
||||
useCallback,
|
||||
useTransition,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
@@ -30,7 +30,7 @@ function Component({x, y, z}) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies:"all"
|
||||
import { useEffect, useEffectEvent } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveEffectDependencies
|
||||
// @validateExhaustiveEffectDependencies:"all"
|
||||
import {useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y, z}) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -167,3 +167,16 @@ function InvalidUseMemo({items}) {
|
||||
const sorted = useMemo(() => [...items].sort(), []);
|
||||
return <div>{sorted.length}</div>;
|
||||
}
|
||||
|
||||
// Invalid: missing/extra deps in useEffect
|
||||
function InvalidEffectDeps({a, b}) {
|
||||
useEffect(() => {
|
||||
console.log(a);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(a);
|
||||
// TODO: eslint-disable-next-line react-hooks/exhaustive-effect-dependencies
|
||||
}, [a, b]);
|
||||
}
|
||||
|
||||
@@ -603,6 +603,18 @@ has-flag@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
version "0.25.1"
|
||||
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
|
||||
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
|
||||
|
||||
hermes-parser@^0.25.1:
|
||||
version "0.25.1"
|
||||
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
|
||||
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
|
||||
dependencies:
|
||||
hermes-estree "0.25.1"
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
@@ -877,12 +889,12 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
"zod-validation-error@^3.0.3 || ^4.0.0":
|
||||
"zod-validation-error@^3.5.0 || ^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
|
||||
"zod@^3.22.4 || ^4.0.0":
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
|
||||
integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
|
||||
"zod@^3.25.0 || ^4.0.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.0.tgz#01e86f2c2b6d525a1b9fa6dbe78beccad082118f"
|
||||
integrity sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==
|
||||
|
||||
@@ -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',
|
||||
@@ -42,6 +139,7 @@ const COMPILER_OPTIONS: PluginOptions = {
|
||||
// Temporarily enabled for internal testing
|
||||
enableUseKeyedState: true,
|
||||
enableVerboseNoSetStateInEffect: true,
|
||||
validateExhaustiveEffectDependencies: 'extra-only',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -215,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')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../../');
|
||||
|
||||
type DebugInfoConfig = {
|
||||
ignoreProps?: boolean,
|
||||
ignoreRscStreamInfo?: boolean,
|
||||
@@ -34,7 +30,7 @@ function normalizeStack(stack) {
|
||||
const [name, file, line, col, enclosingLine, enclosingCol] = stack[i];
|
||||
copy.push([
|
||||
name,
|
||||
file.replace(repoRoot, ''),
|
||||
file.replace(__REACT_ROOT_PATH_TEST__, ''),
|
||||
line,
|
||||
col,
|
||||
enclosingLine,
|
||||
@@ -83,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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user