Compare commits
25 Commits
pr35522
...
asserts-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b9b1fa18 | ||
|
|
41b3e9a670 | ||
|
|
195fd2286b | ||
|
|
d87298ae16 | ||
|
|
be3fb29904 | ||
|
|
23e5edd05c | ||
|
|
3926e2438f | ||
|
|
6baff7ac76 | ||
|
|
bef88f7c11 | ||
|
|
01c4d03d84 | ||
|
|
cbc4d40663 | ||
|
|
db71391c5c | ||
|
|
4cf906380d | ||
|
|
eac3c95537 | ||
|
|
35a81cecf7 | ||
|
|
4028aaa50c | ||
|
|
f0fbb0d199 | ||
|
|
bb8a76c6cc | ||
|
|
fae15df40e | ||
|
|
53daaf5aba | ||
|
|
4a3d993e52 | ||
|
|
3e1abcc8d7 | ||
|
|
c18662405c | ||
|
|
583e200332 | ||
|
|
8a83073753 |
3
.github/workflows/runtime_build_and_test.yml
vendored
3
.github/workflows/runtime_build_and_test.yml
vendored
@@ -278,6 +278,7 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd compiler install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: node --version
|
||||
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
|
||||
|
||||
# Hardcoded to improve parallelism
|
||||
@@ -445,6 +446,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: node --version
|
||||
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
|
||||
|
||||
test_build_devtools:
|
||||
@@ -489,6 +491,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- run: node --version
|
||||
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
|
||||
|
||||
process_artifacts_combined:
|
||||
|
||||
@@ -132,12 +132,6 @@ export class CompilerDiagnostic {
|
||||
return new CompilerDiagnostic({...options, details: []});
|
||||
}
|
||||
|
||||
clone(): CompilerDiagnostic {
|
||||
const cloned = CompilerDiagnostic.create({...this.options});
|
||||
cloned.options.details = [...this.options.details];
|
||||
return cloned;
|
||||
}
|
||||
|
||||
get reason(): CompilerDiagnosticOptions['reason'] {
|
||||
return this.options.reason;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -106,7 +107,6 @@ import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
|
||||
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
|
||||
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
|
||||
import {validateNoImpureValuesInRender} from '../Validation/ValidateNoImpureValuesInRender';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -271,6 +271,10 @@ function runWithEnvironment(
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
@@ -292,15 +296,8 @@ function runWithEnvironment(
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoImpureFunctionsInRender ||
|
||||
env.config.validateRefAccessDuringRender
|
||||
) {
|
||||
validateNoImpureValuesInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
addObject,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, ObjectType, PolyType} from './Types';
|
||||
import {AliasingSignatureConfig, TypeConfig} from './TypeSchema';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {isHookName} from './Environment';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
@@ -626,136 +626,11 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
// TODO: rest of Global objects
|
||||
];
|
||||
|
||||
const RenderHookAliasing: (
|
||||
reason: ValueReason,
|
||||
) => AliasingSignatureConfig = reason => ({
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Freeze the arguments
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
// Render the arguments
|
||||
{
|
||||
kind: 'Render',
|
||||
place: '@rest',
|
||||
},
|
||||
// Returns a frozen value
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Frozen,
|
||||
reason,
|
||||
},
|
||||
// May alias any arguments into the return
|
||||
{
|
||||
kind: 'Alias',
|
||||
from: '@rest',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const EffectHookAliasing: AliasingSignatureConfig = {
|
||||
receiver: '@receiver',
|
||||
params: ['@fn', '@deps'],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: ['@effect'],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@fn',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@deps',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Deps are accessed during render
|
||||
{
|
||||
kind: 'Render',
|
||||
place: '@deps',
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@effect',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@effect',
|
||||
},
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@fn',
|
||||
into: '@effect',
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/*
|
||||
* TODO(mofeiZ): We currently only store rest param effects for hooks.
|
||||
* now that FeatureFlag `enableTreatHooksAsFunctions` is removed we can
|
||||
* use positional params too (?)
|
||||
*/
|
||||
const useEffectEvent = addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltInEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffectEvent',
|
||||
// Frozen because it should not mutate any locally-bound values
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@value'],
|
||||
rest: null,
|
||||
returns: '@return',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
{kind: 'Assign', from: '@value', into: '@return'},
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@value',
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectEventId,
|
||||
);
|
||||
const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
[
|
||||
'useContext',
|
||||
@@ -769,7 +644,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useContext',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.Context,
|
||||
aliasing: RenderHookAliasing(ValueReason.Context),
|
||||
},
|
||||
BuiltInUseContextHookId,
|
||||
),
|
||||
@@ -784,7 +658,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useState',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.State,
|
||||
aliasing: RenderHookAliasing(ValueReason.State),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -797,7 +670,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useActionState',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.State,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -810,7 +682,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useReducer',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.ReducerState,
|
||||
aliasing: RenderHookAliasing(ValueReason.ReducerState),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -844,7 +715,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useMemo',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -856,7 +726,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useCallback',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -870,7 +739,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: EffectHookAliasing,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: ['@effect'],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@effect',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@effect',
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectHookId,
|
||||
),
|
||||
@@ -886,7 +789,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useLayoutEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: EffectHookAliasing,
|
||||
},
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
),
|
||||
@@ -902,7 +804,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useInsertionEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: EffectHookAliasing,
|
||||
},
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
),
|
||||
@@ -916,7 +817,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useTransition',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -929,7 +829,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
hookKind: 'useOptimistic',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.State,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -943,7 +842,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: RenderHookAliasing(ValueReason.HookCaptured),
|
||||
},
|
||||
BuiltInUseOperatorId,
|
||||
),
|
||||
@@ -968,8 +866,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInFireId,
|
||||
),
|
||||
],
|
||||
['useEffectEvent', useEffectEvent],
|
||||
['experimental_useEffectEvent', useEffectEvent],
|
||||
[
|
||||
'useEffectEvent',
|
||||
addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltInEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffectEvent',
|
||||
// Frozen because it should not mutate any locally-bound values
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
|
||||
];
|
||||
|
||||
|
||||
@@ -1879,15 +1879,7 @@ export function isRefValueType(id: Identifier): boolean {
|
||||
}
|
||||
|
||||
export function isUseRefType(id: Identifier): boolean {
|
||||
return isUseRefType_(id.type);
|
||||
}
|
||||
|
||||
export function isUseRefType_(type: Type): boolean {
|
||||
return (
|
||||
(type.kind === 'Object' && type.shapeId === 'BuiltInUseRefId') ||
|
||||
(type.kind === 'Phi' &&
|
||||
type.operands.some(operand => isUseRefType_(operand)))
|
||||
);
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseRefId';
|
||||
}
|
||||
|
||||
export function isUseStateType(id: Identifier): boolean {
|
||||
@@ -1898,13 +1890,6 @@ export function isJsxType(type: Type): boolean {
|
||||
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
|
||||
}
|
||||
|
||||
export function isJsxOrJsxUnionType(type: Type): boolean {
|
||||
return (
|
||||
(type.kind === 'Object' && type.shapeId === 'BuiltInJsx') ||
|
||||
(type.kind === 'Phi' && type.operands.some(op => isJsxOrJsxUnionType(op)))
|
||||
);
|
||||
}
|
||||
|
||||
export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
@@ -2073,23 +2058,4 @@ export function getHookKindForType(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function areEqualSourceLocations(
|
||||
loc1: SourceLocation,
|
||||
loc2: SourceLocation,
|
||||
): boolean {
|
||||
if (typeof loc1 === 'symbol' || typeof loc2 === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
loc1.filename === loc2.filename &&
|
||||
loc1.identifierName === loc2.identifierName &&
|
||||
loc1.start.line === loc2.start.line &&
|
||||
loc1.start.column === loc2.start.column &&
|
||||
loc1.start.index === loc2.start.index &&
|
||||
loc1.end.line === loc2.end.line &&
|
||||
loc1.end.column === loc2.end.column &&
|
||||
loc1.end.index === loc2.end.index
|
||||
);
|
||||
}
|
||||
|
||||
export * from './Types';
|
||||
|
||||
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
loc,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
@@ -190,22 +190,14 @@ function parseAliasingSignatureConfig(
|
||||
};
|
||||
}
|
||||
case 'Impure': {
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Impure',
|
||||
into,
|
||||
category: ErrorCategory.Purity,
|
||||
description: effect.description,
|
||||
reason: effect.reason,
|
||||
sourceMessage: effect.sourceMessage,
|
||||
usageMessage: effect.usageMessage,
|
||||
};
|
||||
}
|
||||
case 'Render': {
|
||||
const place = lookup(effect.place);
|
||||
return {
|
||||
kind: 'Render',
|
||||
kind: 'Impure',
|
||||
place,
|
||||
error: CompilerError.throwTodo({
|
||||
reason: 'Support impure effect declarations',
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'Apply': {
|
||||
@@ -1521,11 +1513,6 @@ export const DefaultNonmutatingHook = addHook(
|
||||
value: '@rest',
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
// Render the arguments
|
||||
{
|
||||
kind: 'Render',
|
||||
place: '@rest',
|
||||
},
|
||||
// Returns a frozen value
|
||||
{
|
||||
kind: 'Create',
|
||||
|
||||
@@ -1009,7 +1009,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.into)} reason=${effect.reason} description=${effect.description}`;
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
|
||||
@@ -185,29 +185,11 @@ export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
|
||||
|
||||
export type ImpureEffectConfig = {
|
||||
kind: 'Impure';
|
||||
into: string;
|
||||
reason: string;
|
||||
description: string;
|
||||
sourceMessage: string;
|
||||
usageMessage: string;
|
||||
place: string;
|
||||
};
|
||||
|
||||
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
|
||||
kind: z.literal('Impure'),
|
||||
into: LifetimeIdSchema,
|
||||
reason: z.string(),
|
||||
description: z.string(),
|
||||
sourceMessage: z.string(),
|
||||
usageMessage: z.string(),
|
||||
});
|
||||
|
||||
export type RenderEffectConfig = {
|
||||
kind: 'Render';
|
||||
place: string;
|
||||
};
|
||||
|
||||
export const RenderEffectSchema: z.ZodType<RenderEffectConfig> = z.object({
|
||||
kind: z.literal('Render'),
|
||||
place: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
@@ -222,8 +204,7 @@ export type AliasingEffectConfig =
|
||||
| ImpureEffectConfig
|
||||
| MutateEffectConfig
|
||||
| MutateTransitiveConditionallyConfig
|
||||
| ApplyEffectConfig
|
||||
| RenderEffectConfig;
|
||||
| ApplyEffectConfig;
|
||||
|
||||
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
|
||||
FreezeEffectSchema,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, ErrorCategory} from '../CompilerError';
|
||||
import {CompilerDiagnostic} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
@@ -162,15 +162,7 @@ export type AliasingEffect =
|
||||
/**
|
||||
* Indicates a side-effect that is not safe during render
|
||||
*/
|
||||
| {
|
||||
kind: 'Impure';
|
||||
into: Place;
|
||||
category: ErrorCategory;
|
||||
reason: string;
|
||||
description: string;
|
||||
usageMessage: string;
|
||||
sourceMessage: string;
|
||||
}
|
||||
| {kind: 'Impure'; place: Place; error: CompilerDiagnostic}
|
||||
/**
|
||||
* Indicates that a given place is accessed during render. Used to distingush
|
||||
* hook arguments that are known to be called immediately vs those used for
|
||||
@@ -230,14 +222,6 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
|
||||
}
|
||||
case 'Impure':
|
||||
return [
|
||||
effect.kind,
|
||||
effect.into.identifier.id,
|
||||
effect.reason,
|
||||
effect.description,
|
||||
effect.usageMessage,
|
||||
effect.sourceMessage,
|
||||
].join(':');
|
||||
case 'Render': {
|
||||
return [effect.kind, effect.place.identifier.id].join(':');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
|
||||
export type ControlDominators = (id: BlockId) => Place | null;
|
||||
export type ControlDominators = (id: BlockId) => boolean;
|
||||
|
||||
/**
|
||||
* Returns an object that lazily calculates whether particular blocks are controlled
|
||||
@@ -23,7 +23,7 @@ export function createControlDominators(
|
||||
});
|
||||
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
|
||||
|
||||
function isControlledBlock(id: BlockId): Place | null {
|
||||
function isControlledBlock(id: BlockId): boolean {
|
||||
let controlBlocks = postDominatorFrontierCache.get(id);
|
||||
if (controlBlocks === undefined) {
|
||||
controlBlocks = postDominatorFrontier(fn, postDominators, id);
|
||||
@@ -35,24 +35,24 @@ export function createControlDominators(
|
||||
case 'if':
|
||||
case 'branch': {
|
||||
if (isControlVariable(controlBlock.terminal.test)) {
|
||||
return controlBlock.terminal.test;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'switch': {
|
||||
if (isControlVariable(controlBlock.terminal.test)) {
|
||||
return controlBlock.terminal.test;
|
||||
return true;
|
||||
}
|
||||
for (const case_ of controlBlock.terminal.cases) {
|
||||
if (case_.test !== null && isControlVariable(case_.test)) {
|
||||
return case_.test;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return isControlledBlock;
|
||||
|
||||
@@ -27,13 +27,11 @@ import {
|
||||
InstructionKind,
|
||||
InstructionValue,
|
||||
isArrayType,
|
||||
isJsxOrJsxUnionType,
|
||||
isJsxType,
|
||||
isMapType,
|
||||
isMutableEffect,
|
||||
isPrimitiveType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
isUseRefType,
|
||||
makeIdentifierId,
|
||||
Phi,
|
||||
Place,
|
||||
@@ -72,7 +70,6 @@ import {
|
||||
MutationReason,
|
||||
} from './AliasingEffects';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {REF_ERROR_DESCRIPTION} from '../Validation/ValidateNoRefAccessInRender';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -572,32 +569,14 @@ function inferBlock(
|
||||
terminal.effects = effects.length !== 0 ? effects : null;
|
||||
}
|
||||
} else if (terminal.kind === 'return') {
|
||||
terminal.effects = [
|
||||
context.internEffect({
|
||||
kind: 'Alias',
|
||||
from: terminal.value,
|
||||
into: context.fn.returns,
|
||||
}),
|
||||
];
|
||||
if (!context.isFuctionExpression) {
|
||||
terminal.effects.push(
|
||||
terminal.effects = [
|
||||
context.internEffect({
|
||||
kind: 'Freeze',
|
||||
value: terminal.value,
|
||||
reason: ValueReason.JsxCaptured,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (
|
||||
context.fn.fnType === 'Component' ||
|
||||
isJsxOrJsxUnionType(context.fn.returns.identifier.type)
|
||||
) {
|
||||
terminal.effects.push(
|
||||
context.internEffect({
|
||||
kind: 'Render',
|
||||
place: terminal.value,
|
||||
}),
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,7 +749,17 @@ function applyEffect(
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
effects.push(effect);
|
||||
const kind = state.kind(effect.from).kind;
|
||||
switch (kind) {
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive: {
|
||||
// no-op: we don't need to track data flow for copy types
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
@@ -1072,17 +1061,6 @@ function applyEffect(
|
||||
reason: new Set(fromValue.reason),
|
||||
});
|
||||
state.define(effect.into, value);
|
||||
applyEffect(
|
||||
context,
|
||||
state,
|
||||
{
|
||||
kind: 'ImmutableCapture',
|
||||
from: effect.from,
|
||||
into: effect.into,
|
||||
},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -1988,11 +1966,6 @@ function computeSignatureForInstruction(
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
from: value.object,
|
||||
into: lvalue,
|
||||
});
|
||||
} else {
|
||||
effects.push({
|
||||
kind: 'CreateFrom',
|
||||
@@ -2000,20 +1973,6 @@ function computeSignatureForInstruction(
|
||||
into: lvalue,
|
||||
});
|
||||
}
|
||||
if (
|
||||
env.config.validateRefAccessDuringRender &&
|
||||
isUseRefType(value.object.identifier)
|
||||
) {
|
||||
effects.push({
|
||||
kind: 'Impure',
|
||||
into: lvalue,
|
||||
category: ErrorCategory.Refs,
|
||||
reason: `Cannot access ref value during render`,
|
||||
description: REF_ERROR_DESCRIPTION,
|
||||
sourceMessage: `Ref is initially accessed`,
|
||||
usageMessage: `Ref value is used during render`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore':
|
||||
@@ -2178,15 +2137,6 @@ function computeSignatureForInstruction(
|
||||
into: lvalue,
|
||||
});
|
||||
}
|
||||
if (value.children != null) {
|
||||
// Children are typically called during render, not used as an event/effect callback
|
||||
for (const child of value.children) {
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place: child,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (value.kind === 'JsxExpression') {
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
// Tags are render function, by definition they're called during render
|
||||
@@ -2195,23 +2145,29 @@ function computeSignatureForInstruction(
|
||||
place: value.tag,
|
||||
});
|
||||
}
|
||||
for (const prop of value.props) {
|
||||
const place =
|
||||
prop.kind === 'JsxAttribute' ? prop.place : prop.argument;
|
||||
if (isUseRefType(place.identifier)) {
|
||||
continue;
|
||||
}
|
||||
if (place.identifier.type.kind === 'Function') {
|
||||
if (isJsxOrJsxUnionType(place.identifier.type.return)) {
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (value.children != null) {
|
||||
// Children are typically called during render, not used as an event/effect callback
|
||||
for (const child of value.children) {
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place,
|
||||
place: child,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const prop of value.props) {
|
||||
if (
|
||||
prop.kind === 'JsxAttribute' &&
|
||||
prop.place.identifier.type.kind === 'Function' &&
|
||||
(isJsxType(prop.place.identifier.type.return) ||
|
||||
(prop.place.identifier.type.return.kind === 'Phi' &&
|
||||
prop.place.identifier.type.return.operands.some(operand =>
|
||||
isJsxType(operand),
|
||||
)))
|
||||
) {
|
||||
// Any props which return jsx are assumed to be called during render
|
||||
effects.push({
|
||||
kind: 'Render',
|
||||
place: prop.place,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2247,11 +2203,6 @@ function computeSignatureForInstruction(
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
from: value.value,
|
||||
into: place,
|
||||
});
|
||||
} else if (patternItem.kind === 'Identifier') {
|
||||
effects.push({
|
||||
kind: 'CreateFrom',
|
||||
@@ -2433,46 +2384,15 @@ function computeSignatureForInstruction(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: lvalue,
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
into: lvalue,
|
||||
from: value.left,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
into: lvalue,
|
||||
from: value.right,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
into: lvalue,
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.Other,
|
||||
});
|
||||
effects.push({
|
||||
kind: 'ImmutableCapture',
|
||||
into: lvalue,
|
||||
from: value.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'Debugger':
|
||||
case 'JSXText':
|
||||
case 'MetaProperty':
|
||||
case 'Primitive':
|
||||
case 'RegExpLiteral':
|
||||
case 'TemplateLiteral':
|
||||
case 'UnaryExpression':
|
||||
case 'UnsupportedNode': {
|
||||
effects.push({
|
||||
kind: 'Create',
|
||||
@@ -2503,7 +2423,7 @@ function computeEffectsForLegacySignature(
|
||||
lvalue: Place,
|
||||
receiver: Place,
|
||||
args: Array<Place | SpreadPattern | Hole>,
|
||||
_loc: SourceLocation,
|
||||
loc: SourceLocation,
|
||||
): Array<AliasingEffect> {
|
||||
const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
@@ -2516,18 +2436,20 @@ function computeEffectsForLegacySignature(
|
||||
if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
|
||||
effects.push({
|
||||
kind: 'Impure',
|
||||
into: lvalue,
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot access impure value during render',
|
||||
description:
|
||||
'Calling an impure function can produce unstable results that update ' +
|
||||
'unpredictably when the component happens to re-render. ' +
|
||||
'(https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
sourceMessage:
|
||||
signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function.`
|
||||
: 'This function is impure',
|
||||
usageMessage: 'Cannot access impure value during render',
|
||||
place: receiver,
|
||||
error: CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (signature.knownIncompatible != null && state.env.enableValidations) {
|
||||
@@ -2826,23 +2748,7 @@ function computeEffectsForSignature(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Impure': {
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
const values = substitutions.get(effect.into.identifier.id) ?? [];
|
||||
for (const value of values) {
|
||||
effects.push({
|
||||
kind: effect.kind,
|
||||
into: value,
|
||||
category: effect.category,
|
||||
reason: effect.reason,
|
||||
description: effect.description,
|
||||
sourceMessage: effect.sourceMessage,
|
||||
usageMessage: effect.usageMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
const values = substitutions.get(effect.place.identifier.id) ?? [];
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ValueReason,
|
||||
Place,
|
||||
isPrimitiveType,
|
||||
isUseRefType,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -29,9 +28,6 @@ import {
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {AliasingEffect, MutationReason} from './AliasingEffects';
|
||||
import {printIdentifier, printType} from '../HIR/PrintHIR';
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
/**
|
||||
* This pass builds an abstract model of the heap and interprets the effects of the
|
||||
@@ -108,6 +104,7 @@ export function inferMutationAliasingRanges(
|
||||
reason: MutationReason | null;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
@@ -200,12 +197,14 @@ export function inferMutationAliasingRanges(
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal'
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
functionEffects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +214,10 @@ export function inferMutationAliasingRanges(
|
||||
state.assign(index, from, into);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
state.assign(index++, block.terminal.value, fn.returns);
|
||||
}
|
||||
|
||||
if (
|
||||
(block.terminal.kind === 'maybe-throw' ||
|
||||
block.terminal.kind === 'return') &&
|
||||
@@ -224,31 +227,23 @@ export function inferMutationAliasingRanges(
|
||||
if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
effect.kind === 'Freeze' || effect.kind === 'Render',
|
||||
{
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: block.terminal.loc,
|
||||
message: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`[${mutation.index}] mutate ${printIdentifier(mutation.place.identifier)}`,
|
||||
);
|
||||
}
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
mutation.place.identifier,
|
||||
@@ -260,16 +255,8 @@ export function inferMutationAliasingRanges(
|
||||
errors,
|
||||
);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(state.debug());
|
||||
}
|
||||
for (const render of renders) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`[${render.index}] render ${printIdentifier(render.place.identifier)}`,
|
||||
);
|
||||
}
|
||||
state.render(render.index, render.place, errors);
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
@@ -528,13 +515,6 @@ export function inferMutationAliasingRanges(
|
||||
const ignoredErrors = new CompilerError();
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node != null && node.render != null) {
|
||||
functionEffects.push({
|
||||
kind: 'Render',
|
||||
place: place,
|
||||
});
|
||||
}
|
||||
tracked.push(place);
|
||||
}
|
||||
for (const into of tracked) {
|
||||
@@ -597,6 +577,7 @@ export function inferMutationAliasingRanges(
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
@@ -631,74 +612,10 @@ type Node = {
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
| {kind: 'Function'; function: HIRFunction};
|
||||
render: Place | null;
|
||||
};
|
||||
|
||||
function _printNode(node: Node): string {
|
||||
const out: Array<string> = [];
|
||||
debugNode(out, node);
|
||||
return out.join('\n');
|
||||
}
|
||||
function debugNode(out: Array<string>, node: Node): void {
|
||||
out.push(
|
||||
printIdentifier(node.id) +
|
||||
printType(node.id.type) +
|
||||
` lastMutated=[${node.lastMutated}]`,
|
||||
);
|
||||
if (node.transitive != null) {
|
||||
out.push(` transitive=${node.transitive.kind}`);
|
||||
}
|
||||
if (node.local != null) {
|
||||
out.push(` local=${node.local.kind}`);
|
||||
}
|
||||
if (node.mutationReason != null) {
|
||||
out.push(` mutationReason=${node.mutationReason?.kind}`);
|
||||
}
|
||||
const edges: Array<{
|
||||
index: number;
|
||||
direction: '<=' | '=>';
|
||||
kind: string;
|
||||
id: Identifier;
|
||||
}> = [];
|
||||
for (const [alias, index] of node.createdFrom) {
|
||||
edges.push({index, direction: '<=', kind: 'createFrom', id: alias});
|
||||
}
|
||||
for (const [alias, index] of node.aliases) {
|
||||
edges.push({index, direction: '<=', kind: 'alias', id: alias});
|
||||
}
|
||||
for (const [alias, index] of node.maybeAliases) {
|
||||
edges.push({index, direction: '<=', kind: 'alias?', id: alias});
|
||||
}
|
||||
for (const [alias, index] of node.captures) {
|
||||
edges.push({index, direction: '<=', kind: 'capture', id: alias});
|
||||
}
|
||||
for (const edge of node.edges) {
|
||||
edges.push({
|
||||
index: edge.index,
|
||||
direction: '=>',
|
||||
kind: edge.kind,
|
||||
id: edge.node,
|
||||
});
|
||||
}
|
||||
edges.sort((a, b) => a.index - b.index);
|
||||
for (const edge of edges) {
|
||||
out.push(
|
||||
` [${edge.index}] ${edge.direction} ${edge.kind} ${printIdentifier(edge.id)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AliasingState {
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
|
||||
debug(): string {
|
||||
const items: Array<string> = [];
|
||||
for (const [_id, node] of this.nodes) {
|
||||
debugNode(items, node);
|
||||
}
|
||||
return items.join('\n');
|
||||
}
|
||||
|
||||
create(place: Place, value: Node['value']): void {
|
||||
this.nodes.set(place.identifier, {
|
||||
id: place.identifier,
|
||||
@@ -712,7 +629,6 @@ class AliasingState {
|
||||
lastMutated: 0,
|
||||
mutationReason: null,
|
||||
value,
|
||||
render: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -765,9 +681,9 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Place, errors: CompilerError): void {
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start.identifier];
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
@@ -775,34 +691,11 @@ class AliasingState {
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null || isUseRefType(node.id)) {
|
||||
if (DEBUG) {
|
||||
console.log(` render ${printIdentifier(current)}: skip mutated/ref`);
|
||||
}
|
||||
if (node == null || node.transitive != null || node.local != null) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
node.local == null &&
|
||||
node.transitive == null &&
|
||||
node.value.kind === 'Function'
|
||||
) {
|
||||
const returns = node.value.function.returns;
|
||||
if (
|
||||
isJsxType(returns.identifier.type) ||
|
||||
(returns.identifier.type.kind === 'Phi' &&
|
||||
returns.identifier.type.operands.some(operand =>
|
||||
isJsxType(operand),
|
||||
))
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(` render ${printIdentifier(current)}: skip function`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (node.render == null) {
|
||||
node.render = start;
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -816,12 +709,6 @@ class AliasingState {
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [alias, when] of node.maybeAliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
|
||||
@@ -167,14 +167,6 @@ export function Set_filter<T>(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Set_subtract<T>(
|
||||
source: ReadonlySet<T>,
|
||||
other: Iterable<T>,
|
||||
): Set<T> {
|
||||
const otherSet = other instanceof Set ? other : new Set(other);
|
||||
return Set_filter(source, item => !otherSet.has(item));
|
||||
}
|
||||
|
||||
export function hasNode<T>(
|
||||
input: NodePath<T | null | undefined>,
|
||||
): input is NodePath<NonNullable<T>> {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 {CompilerDiagnostic, CompilerError} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Checks that known-impure functions are not called during render. Examples of invalid functions to
|
||||
* call during render are `Math.random()` and `Date.now()`. Users may extend this set of
|
||||
* impure functions via a module type provider and specifying functions with `impure: true`.
|
||||
*
|
||||
* TODO: add best-effort analysis of functions which are called during render. We have variations of
|
||||
* this in several of our validation passes and should unify those analyses into a reusable helper
|
||||
* and use it here.
|
||||
*/
|
||||
export function validateNoImpureFunctionsInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
if (value.kind === 'MethodCall' || value.kind == 'CallExpression') {
|
||||
const callee =
|
||||
value.kind === 'MethodCall' ? value.property : value.callee;
|
||||
const signature = getFunctionCallSignature(
|
||||
fn.env,
|
||||
callee.identifier.type,
|
||||
);
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
description:
|
||||
(signature.canonicalName != null
|
||||
? `\`${signature.canonicalName}\` is an impure function. `
|
||||
: '') +
|
||||
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
|
||||
suggestions: null,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: 'Cannot call impure function',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {
|
||||
areEqualSourceLocations,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
isJsxType,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
import {createControlDominators} from '../Inference/ControlDominators';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
assertExhaustive,
|
||||
getOrInsertWith,
|
||||
Set_filter,
|
||||
Set_subtract,
|
||||
} from '../Utils/utils';
|
||||
import {printInstruction} from '../HIR/PrintHIR';
|
||||
|
||||
type ImpureEffect = Extract<AliasingEffect, {kind: 'Impure'}>;
|
||||
type RenderEffect = Extract<AliasingEffect, {kind: 'Render'}>;
|
||||
type FunctionCache = Map<HIRFunction, Map<string, ImpuritySignature>>;
|
||||
type ImpuritySignature = {effects: Array<ImpureEffect>; error: CompilerError};
|
||||
|
||||
export function validateNoImpureValuesInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const impure = new Map<IdentifierId, ImpureEffect>();
|
||||
const result = inferImpureValues(fn, impure, new Map());
|
||||
|
||||
if (result.error.hasAnyErrors()) {
|
||||
return Err(result.error);
|
||||
}
|
||||
return Ok(undefined);
|
||||
}
|
||||
|
||||
function inferFunctionExpressionMemo(
|
||||
fn: HIRFunction,
|
||||
impure: Map<IdentifierId, ImpureEffect>,
|
||||
cache: FunctionCache,
|
||||
): ImpuritySignature {
|
||||
const key = fn.context
|
||||
.map(place => `${place.identifier.id}:${impure.has(place.identifier.id)}`)
|
||||
.join(',');
|
||||
return getOrInsertWith(
|
||||
getOrInsertWith(cache, fn, () => new Map()),
|
||||
key,
|
||||
() => inferImpureValues(fn, impure, cache),
|
||||
);
|
||||
}
|
||||
|
||||
function processEffects(
|
||||
id: InstructionId,
|
||||
effects: Array<AliasingEffect>,
|
||||
impure: Map<IdentifierId, ImpureEffect>,
|
||||
cache: FunctionCache,
|
||||
): boolean {
|
||||
let hasChanges = false;
|
||||
const rendered: Set<IdentifierId> = new Set();
|
||||
for (const effect of effects) {
|
||||
if (effect.kind === 'Render') {
|
||||
rendered.add(effect.place.identifier.id);
|
||||
}
|
||||
}
|
||||
for (const effect of effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Alias':
|
||||
case 'Assign':
|
||||
case 'Capture':
|
||||
case 'CreateFrom':
|
||||
case 'ImmutableCapture': {
|
||||
const sourceEffect = impure.get(effect.from.identifier.id);
|
||||
if (
|
||||
sourceEffect != null &&
|
||||
!impure.has(effect.into.identifier.id) &&
|
||||
!rendered.has(effect.from.identifier.id) &&
|
||||
!isUseRefType(effect.into.identifier) &&
|
||||
!isJsxType(effect.into.identifier.type)
|
||||
) {
|
||||
impure.set(effect.into.identifier.id, sourceEffect);
|
||||
hasChanges = true;
|
||||
}
|
||||
if (
|
||||
sourceEffect == null &&
|
||||
(effect.kind === 'Assign' || effect.kind === 'Capture') &&
|
||||
!impure.has(effect.from.identifier.id) &&
|
||||
!rendered.has(effect.from.identifier.id) &&
|
||||
!isUseRefType(effect.from.identifier) &&
|
||||
isMutable({id}, effect.into)
|
||||
) {
|
||||
const destinationEffect = impure.get(effect.into.identifier.id);
|
||||
if (destinationEffect != null) {
|
||||
impure.set(effect.from.identifier.id, destinationEffect);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Impure': {
|
||||
if (!impure.has(effect.into.identifier.id)) {
|
||||
impure.set(effect.into.identifier.id, effect);
|
||||
hasChanges = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Render': {
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
const result = inferFunctionExpressionMemo(
|
||||
effect.function.loweredFunc.func,
|
||||
impure,
|
||||
cache,
|
||||
);
|
||||
if (result.error.hasAnyErrors()) {
|
||||
break;
|
||||
}
|
||||
const impureEffect: ImpureEffect | null =
|
||||
result.effects.find(
|
||||
(functionEffect: AliasingEffect): functionEffect is ImpureEffect =>
|
||||
functionEffect.kind === 'Impure' &&
|
||||
functionEffect.into.identifier.id ===
|
||||
effect.function.loweredFunc.func.returns.identifier.id,
|
||||
) ?? null;
|
||||
if (impureEffect != null) {
|
||||
impure.set(effect.into.identifier.id, impureEffect);
|
||||
hasChanges = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MaybeAlias':
|
||||
case 'Apply':
|
||||
case 'Create':
|
||||
case 'Freeze':
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
function inferImpureValues(
|
||||
fn: HIRFunction,
|
||||
impure: Map<IdentifierId, ImpureEffect>,
|
||||
cache: FunctionCache,
|
||||
): ImpuritySignature {
|
||||
const getBlockControl = createControlDominators(fn, place => {
|
||||
return impure.has(place.identifier.id);
|
||||
});
|
||||
|
||||
let hasChanges = false;
|
||||
do {
|
||||
hasChanges = false;
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
const controlPlace = getBlockControl(block.id);
|
||||
const controlImpureEffect =
|
||||
controlPlace != null ? impure.get(controlPlace.identifier.id) : null;
|
||||
|
||||
for (const phi of block.phis) {
|
||||
if (impure.has(phi.place.identifier.id)) {
|
||||
// Already marked impure on a previous pass
|
||||
continue;
|
||||
}
|
||||
let impureEffect = null;
|
||||
for (const [, operand] of phi.operands) {
|
||||
const operandEffect = impure.get(operand.identifier.id);
|
||||
if (operandEffect != null) {
|
||||
impureEffect = operandEffect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (impureEffect != null) {
|
||||
impure.set(phi.place.identifier.id, impureEffect);
|
||||
hasChanges = true;
|
||||
} else {
|
||||
for (const [pred] of phi.operands) {
|
||||
const predControl = getBlockControl(pred);
|
||||
if (predControl != null) {
|
||||
const predEffect = impure.get(predControl.identifier.id);
|
||||
if (predEffect != null) {
|
||||
impure.set(phi.place.identifier.id, predEffect);
|
||||
hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
const _impure = new Set(impure.keys());
|
||||
hasChanges =
|
||||
processEffects(instr.id, instr.effects ?? [], impure, cache) ||
|
||||
hasChanges;
|
||||
}
|
||||
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
|
||||
hasChanges =
|
||||
processEffects(
|
||||
block.terminal.id,
|
||||
block.terminal.effects,
|
||||
impure,
|
||||
cache,
|
||||
) || hasChanges;
|
||||
}
|
||||
}
|
||||
} while (hasChanges);
|
||||
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
name: 'ValidateNoImpureValuesInRender',
|
||||
value: JSON.stringify(Array.from(impure.keys()).sort(), null, 2),
|
||||
});
|
||||
|
||||
const error = new CompilerError();
|
||||
function validateRenderEffect(effect: RenderEffect): void {
|
||||
const impureEffect = impure.get(effect.place.identifier.id);
|
||||
if (impureEffect == null) {
|
||||
return;
|
||||
}
|
||||
const diagnostic = CompilerDiagnostic.create({
|
||||
category: impureEffect.category,
|
||||
reason: impureEffect.reason,
|
||||
description: impureEffect.description,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: effect.place.loc,
|
||||
message: impureEffect.usageMessage,
|
||||
});
|
||||
if (!areEqualSourceLocations(effect.place.loc, impureEffect.into.loc)) {
|
||||
diagnostic.withDetails({
|
||||
kind: 'error',
|
||||
loc: impureEffect.into.loc,
|
||||
message: impureEffect.sourceMessage,
|
||||
});
|
||||
}
|
||||
error.pushDiagnostic(diagnostic);
|
||||
}
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
if (
|
||||
value.kind === 'FunctionExpression' ||
|
||||
value.kind === 'ObjectMethod'
|
||||
) {
|
||||
const result = inferFunctionExpressionMemo(
|
||||
value.loweredFunc.func,
|
||||
impure,
|
||||
cache,
|
||||
);
|
||||
if (result.error.hasAnyErrors()) {
|
||||
error.merge(result.error);
|
||||
}
|
||||
}
|
||||
for (const effect of instr.effects ?? []) {
|
||||
if (effect.kind === 'Render') {
|
||||
validateRenderEffect(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return' && block.terminal.effects != null) {
|
||||
for (const effect of block.terminal.effects) {
|
||||
if (effect.kind === 'Render') {
|
||||
validateRenderEffect(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const impureEffects: Array<ImpureEffect> = [];
|
||||
for (const param of [...fn.context, ...fn.params, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const impureEffect = impure.get(place.identifier.id);
|
||||
if (impureEffect != null) {
|
||||
impureEffects.push({
|
||||
kind: 'Impure',
|
||||
into: impureEffect.into,
|
||||
category: impureEffect.category,
|
||||
reason: impureEffect.reason,
|
||||
description: impureEffect.description,
|
||||
sourceMessage: impureEffect.sourceMessage,
|
||||
usageMessage: impureEffect.usageMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {effects: impureEffects, error};
|
||||
}
|
||||
@@ -397,10 +397,16 @@ function validateNoRefAccessInRenderImpl(
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'JsxFragment': {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ComputedLoad':
|
||||
case 'PropertyLoad': {
|
||||
if (instr.value.kind === 'ComputedLoad') {
|
||||
validateNoDirectRefValueAccess(errors, instr.value.property, env);
|
||||
}
|
||||
const objType = env.get(instr.value.object.identifier.id);
|
||||
let lookupType: null | RefAccessType = null;
|
||||
if (objType?.kind === 'Structure') {
|
||||
@@ -493,10 +499,73 @@ function validateNoRefAccessInRenderImpl(
|
||||
instr.value.kind === 'CallExpression'
|
||||
? instr.value.callee
|
||||
: instr.value.property;
|
||||
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
|
||||
let returnType: RefAccessType = {kind: 'None'};
|
||||
const fnType = env.get(callee.identifier.id);
|
||||
let didError = false;
|
||||
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
|
||||
returnType = fnType.fn.returnType;
|
||||
if (fnType.fn.readRefEffect) {
|
||||
didError = true;
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: callee.loc,
|
||||
message: `This function accesses a ref value`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* If we already reported an error on this instruction, don't report
|
||||
* duplicate errors
|
||||
*/
|
||||
if (!didError) {
|
||||
const isRefLValue = isUseRefType(instr.lvalue.identifier);
|
||||
const isEventHandlerLValue = isEventHandlerType(
|
||||
instr.lvalue.identifier,
|
||||
);
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
/**
|
||||
* By default we check that function call operands are not refs,
|
||||
* ref values, or functions that can access refs.
|
||||
*/
|
||||
if (
|
||||
isRefLValue ||
|
||||
isEventHandlerLValue ||
|
||||
(hookKind != null &&
|
||||
hookKind !== 'useState' &&
|
||||
hookKind !== 'useReducer')
|
||||
) {
|
||||
/**
|
||||
* Allow passing refs or ref-accessing functions when:
|
||||
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
|
||||
* 2. lvalue is an event handler (DOM events execute outside render)
|
||||
* 3. calling hooks (independently validated for ref safety)
|
||||
*/
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
} else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
|
||||
/**
|
||||
* Special case: the lvalue is passed as a jsx child
|
||||
*
|
||||
* For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
|
||||
* context and infer that the ref is being passed to a component-like
|
||||
* render function which attempts to obey the rules.
|
||||
*/
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
} else {
|
||||
validateNoRefPassedToFunction(
|
||||
errors,
|
||||
env,
|
||||
operand,
|
||||
operand.loc,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set(instr.lvalue.identifier.id, returnType);
|
||||
break;
|
||||
@@ -505,6 +574,7 @@ function validateNoRefAccessInRenderImpl(
|
||||
case 'ArrayExpression': {
|
||||
const types: Array<RefAccessType> = [];
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
types.push(env.get(operand.identifier.id) ?? {kind: 'None'});
|
||||
}
|
||||
const value = joinRefAccessTypes(...types);
|
||||
@@ -541,10 +611,17 @@ function validateNoRefAccessInRenderImpl(
|
||||
} else {
|
||||
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ComputedDelete' ||
|
||||
instr.value.kind === 'ComputedStore'
|
||||
) {
|
||||
validateNoRefValueAccess(errors, env, instr.value.property);
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ComputedStore' ||
|
||||
instr.value.kind === 'PropertyStore'
|
||||
) {
|
||||
validateNoDirectRefValueAccess(errors, instr.value.value, env);
|
||||
const type = env.get(instr.value.value.identifier.id);
|
||||
if (type != null && type.kind === 'Structure') {
|
||||
let objectType: RefAccessType = type;
|
||||
@@ -585,9 +662,27 @@ function validateNoRefAccessInRenderImpl(
|
||||
* error for the write to the ref
|
||||
*/
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'error',
|
||||
loc: instr.value.value.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
})
|
||||
.withDetails({
|
||||
kind: 'hint',
|
||||
message:
|
||||
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
validateNoRefValueAccess(errors, env, instr.value.value);
|
||||
break;
|
||||
}
|
||||
case 'BinaryExpression': {
|
||||
@@ -609,14 +704,26 @@ function validateNoRefAccessInRenderImpl(
|
||||
|
||||
if (refId !== null && nullish) {
|
||||
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Guard values are derived from ref.current, so they can only be used in if statement targets
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
guardCheck(errors, operand, env);
|
||||
}
|
||||
|
||||
if (
|
||||
isUseRefType(instr.lvalue.identifier) &&
|
||||
env.get(instr.lvalue.identifier.id)?.kind !== 'Ref'
|
||||
@@ -654,8 +761,15 @@ function validateNoRefAccessInRenderImpl(
|
||||
}
|
||||
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (block.terminal.kind === 'return') {
|
||||
if (block.terminal.kind !== 'return') {
|
||||
validateNoRefValueAccess(errors, env, operand);
|
||||
if (block.terminal.kind !== 'if') {
|
||||
guardCheck(errors, operand, env);
|
||||
}
|
||||
} else {
|
||||
// Allow functions containing refs to be returned, but not direct ref values
|
||||
validateNoDirectRefValueAccess(errors, operand, env);
|
||||
guardCheck(errors, operand, env);
|
||||
returnValues.push(env.get(operand.identifier.id));
|
||||
}
|
||||
}
|
||||
@@ -694,6 +808,72 @@ function destructure(
|
||||
return type;
|
||||
}
|
||||
|
||||
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
|
||||
if (env.get(operand.identifier.id)?.kind === 'Guard') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoRefValueAccess(
|
||||
errors: CompilerError,
|
||||
env: Env,
|
||||
operand: Place,
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoRefPassedToFunction(
|
||||
errors: CompilerError,
|
||||
env: Env,
|
||||
operand: Place,
|
||||
loc: SourceLocation,
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (
|
||||
type?.kind === 'Ref' ||
|
||||
type?.kind === 'RefValue' ||
|
||||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
|
||||
) {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
message: `Passing a ref to a function may read its value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoRefUpdate(
|
||||
errors: CompilerError,
|
||||
env: Env,
|
||||
@@ -706,7 +886,7 @@ function validateNoRefUpdate(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: REF_ERROR_DESCRIPTION,
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: (type.kind === 'RefValue' && type.loc) || loc,
|
||||
@@ -716,7 +896,28 @@ function validateNoRefUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
export const REF_ERROR_DESCRIPTION =
|
||||
function validateNoDirectRefValueAccess(
|
||||
errors: CompilerError,
|
||||
operand: Place,
|
||||
env: Env,
|
||||
): void {
|
||||
const type = destructure(env.get(operand.identifier.id));
|
||||
if (type?.kind === 'RefValue') {
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Refs,
|
||||
reason: 'Cannot access refs during render',
|
||||
description: ERROR_DESCRIPTION,
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: type.loc ?? operand.loc,
|
||||
message: `Cannot access ref value during render`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_DESCRIPTION =
|
||||
'React refs are values that are not needed for rendering. Refs should only be accessed ' +
|
||||
'outside of render, such as in event handlers or effects. ' +
|
||||
'Accessing a ref value (the `current` property) during render can cause your component ' +
|
||||
|
||||
@@ -202,10 +202,10 @@ function getSetStateCall(
|
||||
);
|
||||
};
|
||||
|
||||
const isRefControlledBlock: (id: BlockId) => Place | null =
|
||||
const isRefControlledBlock: (id: BlockId) => boolean =
|
||||
enableAllowSetStateFromRefsInEffects
|
||||
? createControlDominators(fn, place => isDerivedFromRef(place))
|
||||
: (): Place | null => null;
|
||||
: (): boolean => false;
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (enableAllowSetStateFromRefsInEffects) {
|
||||
@@ -224,7 +224,7 @@ function getSetStateCall(
|
||||
refDerivedValues.add(phi.place.identifier.id);
|
||||
} else {
|
||||
for (const [pred] of phi.operands) {
|
||||
if (isRefControlledBlock(pred) != null) {
|
||||
if (isRefControlledBlock(pred)) {
|
||||
refDerivedValues.add(phi.place.identifier.id);
|
||||
break;
|
||||
}
|
||||
@@ -337,7 +337,7 @@ function getSetStateCall(
|
||||
* be needed when initial layout measurements from refs need to be stored in state.
|
||||
*/
|
||||
return null;
|
||||
} else if (isRefControlledBlock(block.id) != null) {
|
||||
} else if (isRefControlledBlock(block.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(props.value);
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useRef } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(1);
|
||||
const ref = useRef(props.value);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
t0 = <Stringify object={object} shouldInvokeFns={true} />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>
|
||||
@@ -5,7 +5,6 @@
|
||||
function Component() {
|
||||
const Foo = () => {
|
||||
someGlobal = true;
|
||||
return <div />;
|
||||
};
|
||||
return <Foo />;
|
||||
}
|
||||
@@ -27,9 +26,9 @@ error.assign-global-in-component-tag-function.ts:3:4
|
||||
2 | const Foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
4 | return <div />;
|
||||
5 | };
|
||||
6 | return <Foo />;
|
||||
4 | };
|
||||
5 | return <Foo />;
|
||||
6 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
function Component() {
|
||||
const Foo = () => {
|
||||
someGlobal = true;
|
||||
return <div />;
|
||||
};
|
||||
return <Foo />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
return <div />;
|
||||
};
|
||||
// Children are generally access/called during render, so
|
||||
// modifying a global in a children function is almost
|
||||
@@ -30,9 +29,9 @@ error.assign-global-in-jsx-children.ts:3:4
|
||||
2 | const foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ `someGlobal` cannot be reassigned
|
||||
4 | return <div />;
|
||||
5 | };
|
||||
6 | // Children are generally access/called during render, so
|
||||
4 | };
|
||||
5 | // Children are generally access/called during render, so
|
||||
6 | // modifying a global in a children function is almost
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
return <div />;
|
||||
};
|
||||
// Children are generally access/called during render, so
|
||||
// modifying a global in a children function is almost
|
||||
|
||||
@@ -20,26 +20,30 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.hook-ref-value.ts:5:22
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^^^ Ref value is used during render
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
error.hook-ref-value.ts:5:23
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.hook-ref-value.ts:5:23
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | useEffect(() => {}, [ref.current]);
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -17,23 +17,15 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-access-ref-during-render.ts:5:9
|
||||
3 | const ref = useRef(null);
|
||||
4 | const value = ref.current;
|
||||
> 5 | return value;
|
||||
| ^^^^^ Ref value is used during render
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
error.invalid-access-ref-during-render.ts:4:16
|
||||
2 | function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
5 | return value;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
@@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -36,16 +36,7 @@ error.invalid-access-ref-in-reducer-init.ts:8:4
|
||||
6 | (state, action) => state + action,
|
||||
7 | 0,
|
||||
> 8 | init => ref.current
|
||||
| ^^^^^^^^^^^^^^^^^^^ Ref value is used during render
|
||||
9 | );
|
||||
10 |
|
||||
11 | return <Stringify state={state} />;
|
||||
|
||||
error.invalid-access-ref-in-reducer-init.ts:8:12
|
||||
6 | (state, action) => state + action,
|
||||
7 | 0,
|
||||
> 8 | init => ref.current
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
9 | );
|
||||
10 |
|
||||
11 | return <Stringify state={state} />;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -32,16 +32,7 @@ error.invalid-access-ref-in-reducer.ts:5:29
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef(props.value);
|
||||
> 5 | const [state] = useReducer(() => ref.current, null);
|
||||
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
|
||||
6 |
|
||||
7 | return <Stringify state={state} />;
|
||||
8 | }
|
||||
|
||||
error.invalid-access-ref-in-reducer.ts:5:35
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef(props.value);
|
||||
> 5 | const [state] = useReducer(() => ref.current, null);
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
6 |
|
||||
7 | return <Stringify state={state} />;
|
||||
8 | }
|
||||
|
||||
@@ -20,26 +20,18 @@ function Component() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:8:15
|
||||
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19
|
||||
5 | const object = {};
|
||||
6 | object.foo = () => ref.current;
|
||||
7 | const refValue = object.foo();
|
||||
> 8 | return <div>{refValue}</div>;
|
||||
| ^^^^^^^^ Ref value is used during render
|
||||
> 7 | const refValue = object.foo();
|
||||
| ^^^^^^^^^^ This function accesses a ref value
|
||||
8 | return <div>{refValue}</div>;
|
||||
9 | }
|
||||
10 |
|
||||
|
||||
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:6:21
|
||||
4 | const ref = useRef(null);
|
||||
5 | const object = {};
|
||||
> 6 | object.foo = () => ref.current;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
7 | const refValue = object.foo();
|
||||
8 | return <div>{refValue}</div>;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -32,16 +32,7 @@ error.invalid-access-ref-in-state-initializer.ts:5:27
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef(props.value);
|
||||
> 5 | const [state] = useState(() => ref.current);
|
||||
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
|
||||
6 |
|
||||
7 | return <Stringify state={state} />;
|
||||
8 | }
|
||||
|
||||
error.invalid-access-ref-in-state-initializer.ts:5:33
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef(props.value);
|
||||
> 5 | const [state] = useState(() => ref.current);
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
|
||||
6 |
|
||||
7 | return <Stringify state={state} />;
|
||||
8 | }
|
||||
|
||||
@@ -21,27 +21,17 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:7:37
|
||||
5 | const aliasedRef = ref;
|
||||
6 | const current = aliasedRef.current;
|
||||
> 7 | return <Foo item={item} current={current} />;
|
||||
| ^^^^^^^ Ref value is used during render
|
||||
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33
|
||||
7 | return <Foo item={item} current={current} />;
|
||||
8 | };
|
||||
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
|
||||
10 | }
|
||||
|
||||
error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:6:20
|
||||
4 | const renderItem = item => {
|
||||
5 | const aliasedRef = ref;
|
||||
> 6 | const current = aliasedRef.current;
|
||||
| ^^^^^^^^^^^^^^^^^^ Ref is initially accessed
|
||||
7 | return <Foo item={item} current={current} />;
|
||||
8 | };
|
||||
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
11 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const ref = useRef(props.value);
|
||||
const object = {};
|
||||
object.foo = () => ref.current;
|
||||
return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-capturing-ref-returning-function-in-rendered-object.ts:8:28
|
||||
6 | const object = {};
|
||||
7 | object.foo = () => ref.current;
|
||||
> 8 | return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
| ^^^^^^ Ref value is used during render
|
||||
9 | }
|
||||
10 |
|
||||
11 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
error.invalid-capturing-ref-returning-function-in-rendered-object.ts:7:21
|
||||
5 | const ref = useRef(props.value);
|
||||
6 | const object = {};
|
||||
> 7 | object.foo = () => ref.current;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
8 | return <Stringify object={object} shouldInvokeFns={true} />;
|
||||
9 | }
|
||||
10 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {arrayPush, identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const getDate = () => Date.now();
|
||||
const now = getDate();
|
||||
const array = [];
|
||||
arrayPush(array, now);
|
||||
return <Foo hasDate={array} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-indirect-via-mutation.ts:10:23
|
||||
8 | const array = [];
|
||||
9 | arrayPush(array, now);
|
||||
> 10 | return <Foo hasDate={array} />;
|
||||
| ^^^^^ Cannot access impure value during render
|
||||
11 | }
|
||||
12 |
|
||||
|
||||
error.invalid-impure-functions-in-render-indirect-via-mutation.ts:6:24
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const getDate = () => Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const now = getDate();
|
||||
8 | const array = [];
|
||||
9 | arrayPush(array, now);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {arrayPush, identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const getDate = () => Date.now();
|
||||
const now = getDate();
|
||||
const array = [];
|
||||
arrayPush(array, now);
|
||||
return <Foo hasDate={array} />;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const getDate = () => Date.now();
|
||||
const array = makeArray(getDate());
|
||||
const hasDate = identity(array);
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-indirect.ts:9:23
|
||||
7 | const array = makeArray(getDate());
|
||||
8 | const hasDate = identity(array);
|
||||
> 9 | return <Foo hasDate={hasDate} />;
|
||||
| ^^^^^^^ Cannot access impure value during render
|
||||
10 | }
|
||||
11 |
|
||||
|
||||
error.invalid-impure-functions-in-render-indirect.ts:6:24
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const getDate = () => Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const array = makeArray(getDate());
|
||||
8 | const hasDate = identity(array);
|
||||
9 | return <Foo hasDate={hasDate} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const getDate = () => Date.now();
|
||||
const array = makeArray(getDate());
|
||||
const hasDate = identity(array);
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = () => Date.now();
|
||||
const f = () => {
|
||||
// this should error but we currently lose track of the impurity bc
|
||||
// the impure value comes from behind a call
|
||||
const array = makeArray(now());
|
||||
const hasDate = identity(array);
|
||||
return hasDate;
|
||||
};
|
||||
const hasDate = f();
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-via-function-call-2.ts:15:23
|
||||
13 | };
|
||||
14 | const hasDate = f();
|
||||
> 15 | return <Foo hasDate={hasDate} />;
|
||||
| ^^^^^^^ Cannot access impure value during render
|
||||
16 | }
|
||||
17 |
|
||||
|
||||
error.invalid-impure-functions-in-render-via-function-call-2.ts:6:20
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const now = () => Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const f = () => {
|
||||
8 | // this should error but we currently lose track of the impurity bc
|
||||
9 | // the impure value comes from behind a call
|
||||
```
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = () => Date.now();
|
||||
const f = () => {
|
||||
// this should error but we currently lose track of the impurity bc
|
||||
// the impure value comes from behind a call
|
||||
const array = makeArray(now());
|
||||
const hasDate = identity(array);
|
||||
return hasDate;
|
||||
};
|
||||
const hasDate = f();
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const f = () => {
|
||||
const array = makeArray(now);
|
||||
const hasDate = identity(array);
|
||||
return hasDate;
|
||||
};
|
||||
const hasDate = f();
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-via-function-call.ts:13:23
|
||||
11 | };
|
||||
12 | const hasDate = f();
|
||||
> 13 | return <Foo hasDate={hasDate} />;
|
||||
| ^^^^^^^ Cannot access impure value during render
|
||||
14 | }
|
||||
15 |
|
||||
|
||||
error.invalid-impure-functions-in-render-via-function-call.ts:6:14
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const now = Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const f = () => {
|
||||
8 | const array = makeArray(now);
|
||||
9 | const hasDate = identity(array);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const f = () => {
|
||||
const array = makeArray(now);
|
||||
const hasDate = identity(array);
|
||||
return hasDate;
|
||||
};
|
||||
const hasDate = f();
|
||||
return <Foo hasDate={hasDate} />;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {typedArrayPush, typedIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const renderItem = () => {
|
||||
const array = [];
|
||||
typedArrayPush(array, now());
|
||||
const hasDate = typedIdentity(array);
|
||||
return <Bar hasDate={hasDate} />;
|
||||
};
|
||||
return <Foo renderItem={renderItem} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-via-render-helper-typed.ts:13:26
|
||||
11 | return <Bar hasDate={hasDate} />;
|
||||
12 | };
|
||||
> 13 | return <Foo renderItem={renderItem} />;
|
||||
| ^^^^^^^^^^ Cannot access impure value during render
|
||||
14 | }
|
||||
15 |
|
||||
|
||||
error.invalid-impure-functions-in-render-via-render-helper-typed.ts:6:14
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const now = Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const renderItem = () => {
|
||||
8 | const array = [];
|
||||
9 | typedArrayPush(array, now());
|
||||
```
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {typedArrayPush, typedIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const renderItem = () => {
|
||||
const array = [];
|
||||
typedArrayPush(array, now());
|
||||
const hasDate = typedIdentity(array);
|
||||
return <Bar hasDate={hasDate} />;
|
||||
};
|
||||
return <Foo renderItem={renderItem} />;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const renderItem = () => {
|
||||
const array = makeArray(now);
|
||||
const hasDate = identity(array);
|
||||
return <Bar hasDate={hasDate} />;
|
||||
};
|
||||
return <Foo renderItem={renderItem} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render-via-render-helper.ts:12:26
|
||||
10 | return <Bar hasDate={hasDate} />;
|
||||
11 | };
|
||||
> 12 | return <Foo renderItem={renderItem} />;
|
||||
| ^^^^^^^^^^ Cannot access impure value during render
|
||||
13 | }
|
||||
14 |
|
||||
|
||||
error.invalid-impure-functions-in-render-via-render-helper.ts:6:14
|
||||
4 |
|
||||
5 | function Component() {
|
||||
> 6 | const now = Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
7 | const renderItem = () => {
|
||||
8 | const array = makeArray(now);
|
||||
9 | const hasDate = identity(array);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
|
||||
import {identity, makeArray} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const now = Date.now();
|
||||
const renderItem = () => {
|
||||
const array = makeArray(now);
|
||||
const hasDate = identity(array);
|
||||
return <Bar hasDate={hasDate} />;
|
||||
};
|
||||
return <Foo renderItem={renderItem} />;
|
||||
}
|
||||
@@ -19,65 +19,41 @@ function Component() {
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:20
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const date = Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
| ^^^^^^^^^^ Cannot call impure function
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:31
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:5:14
|
||||
3 | function Component() {
|
||||
4 | const date = Date.now();
|
||||
> 5 | const now = performance.now();
|
||||
| ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function.
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:42
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:6:15
|
||||
4 | const date = Date.now();
|
||||
5 | const now = performance.now();
|
||||
> 6 | const rand = Math.random();
|
||||
| ^^^^^^^^^^^^^ `Math.random` is an impure function.
|
||||
| ^^^^^^^^^^^^^ Cannot call impure function
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
function Component() {
|
||||
const now = () => Date.now();
|
||||
const render = () => {
|
||||
return <div>{now()}</div>;
|
||||
};
|
||||
return <div>{render()}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-value-in-render-helper.ts:5:17
|
||||
3 | const now = () => Date.now();
|
||||
4 | const render = () => {
|
||||
> 5 | return <div>{now()}</div>;
|
||||
| ^^^^^ Cannot access impure value during render
|
||||
6 | };
|
||||
7 | return <div>{render()}</div>;
|
||||
8 | }
|
||||
|
||||
error.invalid-impure-value-in-render-helper.ts:3:20
|
||||
1 | // @validateNoImpureFunctionsInRender
|
||||
2 | function Component() {
|
||||
> 3 | const now = () => Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
4 | const render = () => {
|
||||
5 | return <div>{now()}</div>;
|
||||
6 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
function Component() {
|
||||
const now = () => Date.now();
|
||||
const render = () => {
|
||||
return <div>{now()}</div>;
|
||||
};
|
||||
return <div>{render()}</div>;
|
||||
}
|
||||
@@ -16,23 +16,15 @@ function Component({ref}) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-read-ref-prop-in-render-destructure.ts:4:15
|
||||
2 | function Component({ref}) {
|
||||
3 | const value = ref.current;
|
||||
> 4 | return <div>{value}</div>;
|
||||
| ^^^^^ Ref value is used during render
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
error.invalid-read-ref-prop-in-render-destructure.ts:3:16
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
2 | function Component({ref}) {
|
||||
> 3 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
4 | return <div>{value}</div>;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -16,23 +16,15 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-read-ref-prop-in-render-property-load.ts:4:15
|
||||
2 | function Component(props) {
|
||||
3 | const value = props.ref.current;
|
||||
> 4 | return <div>{value}</div>;
|
||||
| ^^^^^ Ref value is used during render
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
error.invalid-read-ref-prop-in-render-property-load.ts:3:16
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
2 | function Component(props) {
|
||||
> 3 | const value = props.ref.current;
|
||||
| ^^^^^^^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
|
||||
4 | return <div>{value}</div>;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -22,27 +22,57 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 4 errors:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^^ Cannot access ref value during render
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Ref value is used during render
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
4 | component C() {
|
||||
5 | const r = useRef(null);
|
||||
> 6 | const current = !r.current;
|
||||
| ^^^^^^^^^ Ref is initially accessed
|
||||
7 | return <div>{current}</div>;
|
||||
8 | }
|
||||
9 |
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
5 | const r = useRef(null);
|
||||
6 | const current = !r.current;
|
||||
> 7 | return <div>{current}</div>;
|
||||
| ^^^^^^^ Cannot access ref value during render
|
||||
8 | }
|
||||
9 |
|
||||
10 | export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
|
||||
@@ -20,27 +20,17 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-ref-in-callback-invoked-during-render.ts:6:37
|
||||
4 | const renderItem = item => {
|
||||
5 | const current = ref.current;
|
||||
> 6 | return <Foo item={item} current={current} />;
|
||||
| ^^^^^^^ Ref value is used during render
|
||||
7 | };
|
||||
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
9 | }
|
||||
|
||||
error.invalid-ref-in-callback-invoked-during-render.ts:5:20
|
||||
3 | const ref = useRef(null);
|
||||
4 | const renderItem = item => {
|
||||
> 5 | const current = ref.current;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
6 | return <Foo item={item} current={current} />;
|
||||
7 | };
|
||||
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
error.invalid-ref-in-callback-invoked-during-render.ts:8:33
|
||||
6 | return <Foo item={item} current={current} />;
|
||||
7 | };
|
||||
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
|
||||
9 | }
|
||||
10 |
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ function Component(props) {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -24,7 +24,7 @@ error.invalid-ref-value-as-props.ts:4:19
|
||||
2 | function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | return <Foo ref={ref.current} />;
|
||||
| ^^^^^^^^^^^ Ref value is used during render
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
5 | }
|
||||
6 |
|
||||
```
|
||||
|
||||
@@ -15,9 +15,22 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-set-and-read-ref-during-render.ts:4:2
|
||||
2 | function Component(props) {
|
||||
3 | const ref = useRef(null);
|
||||
> 4 | ref.current = props.value;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
5 | return ref.current;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -25,7 +38,7 @@ error.invalid-set-and-read-ref-during-render.ts:5:9
|
||||
3 | const ref = useRef(null);
|
||||
4 | ref.current = props.value;
|
||||
> 5 | return ref.current;
|
||||
| ^^^^^^^^^^^ Ref value is used during render
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
@@ -15,9 +15,22 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.invalid-set-and-read-ref-nested-property-during-render.ts:4:2
|
||||
2 | function Component(props) {
|
||||
3 | const ref = useRef({inner: null});
|
||||
> 4 | ref.current.inner = props.value;
|
||||
| ^^^^^^^^^^^ Cannot update ref during render
|
||||
5 | return ref.current.inner;
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -25,15 +38,7 @@ error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9
|
||||
3 | const ref = useRef({inner: null});
|
||||
4 | ref.current.inner = props.value;
|
||||
> 5 | return ref.current.inner;
|
||||
| ^^^^^^^^^^^^^^^^^ Ref value is used during render
|
||||
6 | }
|
||||
7 |
|
||||
|
||||
error.invalid-set-and-read-ref-nested-property-during-render.ts:5:9
|
||||
3 | const ref = useRef({inner: null});
|
||||
4 | ref.current.inner = props.value;
|
||||
> 5 | return ref.current.inner;
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
|
||||
6 | }
|
||||
7 |
|
||||
```
|
||||
|
||||
@@ -25,7 +25,19 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
Found 2 errors:
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
6 | component C() {
|
||||
7 | const r = useRef(DEFAULT_VALUE);
|
||||
> 8 | if (r.current == DEFAULT_VALUE) {
|
||||
| ^^^^^^^^^ Cannot access ref value during render
|
||||
9 | r.current = 1;
|
||||
10 | }
|
||||
11 | }
|
||||
|
||||
Error: Cannot access refs during render
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
@@ -30,7 +30,7 @@ error.ref-optional.ts:5:9
|
||||
3 | function Component(props) {
|
||||
4 | const ref = useRef();
|
||||
> 5 | return ref?.current;
|
||||
| ^^^^^^^^^^^^ Ref value is used during render
|
||||
| ^^^^^^^^^^^^ Cannot access ref value during render
|
||||
6 | }
|
||||
7 |
|
||||
8 | export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -38,24 +38,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Cannot access ref value during render
|
||||
Error: Cannot access refs during render
|
||||
|
||||
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
|
||||
|
||||
error.ref-value-in-event-handler-wrapper.ts:19:6
|
||||
17 | <>
|
||||
18 | <input ref={ref} />
|
||||
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ref value is used during render
|
||||
20 | </>
|
||||
21 | );
|
||||
22 | }
|
||||
|
||||
error.ref-value-in-event-handler-wrapper.ts:19:35
|
||||
17 | <>
|
||||
18 | <input ref={ref} />
|
||||
> 19 | <button onClick={handleClick(ref.current)}>Click</button>
|
||||
| ^^^^^^^^^^^ Ref is initially accessed
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
20 | </>
|
||||
21 | );
|
||||
22 | }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
|
||||
// @validateExhaustiveMemoizationDependencies
|
||||
import {useMemo} from 'react';
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -19,65 +19,41 @@ function Component() {
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:20
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const date = Date.now();
|
||||
| ^^^^^^^^^^ `Date.now` is an impure function.
|
||||
| ^^^^^^^^^^ Cannot call impure function
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:31
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:5:14
|
||||
3 | function Component() {
|
||||
4 | const date = Date.now();
|
||||
> 5 | const now = performance.now();
|
||||
| ^^^^^^^^^^^^^^^^^ `performance.now` is an impure function.
|
||||
| ^^^^^^^^^^^^^^^^^ Cannot call impure function
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
|
||||
Error: Cannot access impure value during render
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:7:42
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
> 7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
| ^^^^ Cannot access impure value during render
|
||||
8 | }
|
||||
9 |
|
||||
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:6:15
|
||||
4 | const date = Date.now();
|
||||
5 | const now = performance.now();
|
||||
> 6 | const rand = Math.random();
|
||||
| ^^^^^^^^^^^^^ `Math.random` is an impure function.
|
||||
| ^^^^^^^^^^^^^ Cannot call impure function
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
hook useFoo() {
|
||||
component Foo() {
|
||||
const ref = useRef();
|
||||
|
||||
const s = () => {
|
||||
@@ -16,10 +16,6 @@ hook useFoo() {
|
||||
return s;
|
||||
}
|
||||
|
||||
component Foo() {
|
||||
useFoo();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
@@ -34,7 +30,7 @@ import { c as _c } from "react/compiler-runtime";
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
function useFoo() {
|
||||
function Foo() {
|
||||
const $ = _c(1);
|
||||
const ref = useRef();
|
||||
let t0;
|
||||
@@ -48,10 +44,6 @@ function useFoo() {
|
||||
return s;
|
||||
}
|
||||
|
||||
function Foo() {
|
||||
useFoo();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
@@ -60,4 +52,4 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok)
|
||||
(kind: ok) "[[ function params=0 ]]"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {useRef} from 'react';
|
||||
|
||||
hook useFoo() {
|
||||
component Foo() {
|
||||
const ref = useRef();
|
||||
|
||||
const s = () => {
|
||||
@@ -12,10 +12,6 @@ hook useFoo() {
|
||||
return s;
|
||||
}
|
||||
|
||||
component Foo() {
|
||||
useFoo();
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [],
|
||||
|
||||
@@ -25,40 +25,22 @@ component Component(prop1, ref) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c, useFire } from "react/compiler-runtime";
|
||||
import { useFire } from "react/compiler-runtime";
|
||||
import { fire } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
const Component = React.forwardRef(Component_withRef);
|
||||
function Component_withRef(t0, ref) {
|
||||
const $ = _c(5);
|
||||
const { prop1 } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop1) {
|
||||
t1 = () => {
|
||||
console.log(prop1);
|
||||
};
|
||||
$[0] = prop1;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const foo = t1;
|
||||
const t2 = useFire(foo);
|
||||
let t3;
|
||||
if ($[2] !== prop1 || $[3] !== t2) {
|
||||
t3 = () => {
|
||||
t2(prop1);
|
||||
bar();
|
||||
t2();
|
||||
};
|
||||
$[2] = prop1;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
useEffect(t3);
|
||||
const foo = () => {
|
||||
console.log(prop1);
|
||||
};
|
||||
const t1 = useFire(foo);
|
||||
useEffect(() => {
|
||||
t1(prop1);
|
||||
bar();
|
||||
t1();
|
||||
});
|
||||
print(ref.current);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoImpureFunctionsInRender
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const f = () => Math.random();
|
||||
const ref = useRef(f());
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoImpureFunctionsInRender
|
||||
import { useIdentity } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const f = _temp;
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = f();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const ref = useRef(t0);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <div ref={ref} />;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
function _temp() {
|
||||
return Math.random();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -1,8 +0,0 @@
|
||||
// @validateNoImpureFunctionsInRender
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const f = () => Math.random();
|
||||
const ref = useRef(f());
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
@@ -29,9 +29,9 @@ testRule(
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
makeTestCaseError('Cannot access impure value during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
makeTestCaseError('Cannot call impure function during render'),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -171,17 +171,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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
if (__DEV__) {
|
||||
console.warn('Hello\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello']);
|
||||
assertConsoleWarnDev(['Hello\n in div']);
|
||||
});
|
||||
|
||||
it('passes if all warnings contain a stack', () => {
|
||||
@@ -888,7 +888,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails if act is called without assertConsoleWarnDev', async () => {
|
||||
@@ -1075,7 +1079,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi \n in div');
|
||||
console.warn('Wow \n in div');
|
||||
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
@@ -1085,9 +1093,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hi
|
||||
- Wow
|
||||
- Bye
|
||||
- Hi in div
|
||||
- Wow in div
|
||||
- Bye in div
|
||||
+ Hi in div (at **)
|
||||
+ Wow in div (at **)"
|
||||
`);
|
||||
@@ -1188,16 +1196,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye\n in div');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello
|
||||
+ Good day in div (at **)
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1207,16 +1225,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye\n in div');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello in div (at **)
|
||||
+ Good day
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1226,41 +1254,26 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Hello\n in div');
|
||||
console.warn('Good day\n in div');
|
||||
console.warn('Bye');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
`);
|
||||
});
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if all warnings do not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hello');
|
||||
console.warn('Good day');
|
||||
console.warn('Bye');
|
||||
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this warning should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
|
||||
- Hello in div
|
||||
- Good day in div
|
||||
- Bye in div
|
||||
+ Hello in div (at **)
|
||||
+ Good day in div (at **)
|
||||
+ Bye"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1339,12 +1352,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1361,16 +1375,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1382,9 +1396,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.warn('Bye\n in div');
|
||||
}
|
||||
assertConsoleWarnDev([
|
||||
'Hello',
|
||||
'Hello\n in div',
|
||||
['Good day', {withoutStack: true}],
|
||||
'Bye',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1490,12 +1504,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1524,16 +1539,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
If this warning should include a component stack, remove {withoutStack: true} from this warning.
|
||||
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1606,13 +1621,18 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
it('fails if component stack is passed twice', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi %s%s', '\n in div', '\n in div');
|
||||
assertConsoleWarnDev(['Hi']);
|
||||
assertConsoleWarnDev(['Hi \n in div (at **)']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s""
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1621,16 +1641,23 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.warn('Hi %s%s', '\n in div', '\n in div');
|
||||
console.warn('Bye %s%s', '\n in div', '\n in div');
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div (at **)',
|
||||
'Bye \n in div (at **)',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleWarnDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s"
|
||||
Unexpected warning(s) recorded.
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Bye %s%s""
|
||||
- Expected warnings
|
||||
+ Received warnings
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)
|
||||
Bye in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -1646,7 +1673,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -1661,7 +1688,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Bye']);
|
||||
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -1677,7 +1704,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleWarnDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail if waitFor is called before asserting', async () => {
|
||||
@@ -1884,7 +1915,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
if (__DEV__) {
|
||||
console.error('Hello\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello']);
|
||||
assertConsoleErrorDev(['Hello\n in div']);
|
||||
});
|
||||
|
||||
it('passes if all errors contain a stack', () => {
|
||||
@@ -1893,7 +1924,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hello\n in div',
|
||||
'Good day\n in div',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails if act is called without assertConsoleErrorDev', async () => {
|
||||
@@ -2080,7 +2115,11 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi \n in div');
|
||||
console.error('Wow \n in div');
|
||||
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
@@ -2090,9 +2129,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hi
|
||||
- Wow
|
||||
- Bye
|
||||
- Hi in div
|
||||
- Wow in div
|
||||
- Bye in div
|
||||
+ Hi in div (at **)
|
||||
+ Wow in div (at **)"
|
||||
`);
|
||||
@@ -2192,101 +2231,6 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
+ TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if only error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
assertConsoleErrorDev(['Hello']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if first error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if last error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
console.error('Good day\n in div');
|
||||
console.error('Bye\n in div');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if middle error does not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello\n in div');
|
||||
console.error('Good day');
|
||||
console.error('Bye\n in div');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
// @gate __DEV__
|
||||
it('fails if all errors do not contain a stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hello');
|
||||
console.error('Good day');
|
||||
console.error('Bye');
|
||||
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing component stack for:
|
||||
"Hello"
|
||||
|
||||
Missing component stack for:
|
||||
"Good day"
|
||||
|
||||
Missing component stack for:
|
||||
"Bye"
|
||||
|
||||
If this error should omit a component stack, pass [log, {withoutStack: true}].
|
||||
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('regression: checks entire string, not just the first letter', async () => {
|
||||
@@ -2385,12 +2329,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2407,16 +2352,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -2428,9 +2373,9 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
console.error('Bye\n in div');
|
||||
}
|
||||
assertConsoleErrorDev([
|
||||
'Hello',
|
||||
'Hello\n in div',
|
||||
['Good day', {withoutStack: true}],
|
||||
'Bye',
|
||||
'Bye\n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2536,12 +2481,13 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
- Hello
|
||||
+ Hello in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2570,16 +2516,16 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Unexpected component stack for:
|
||||
"Hello
|
||||
in div (at **)"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Unexpected component stack for:
|
||||
"Bye
|
||||
in div (at **)"
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
If this error should include a component stack, remove {withoutStack: true} from this error.
|
||||
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
|
||||
- Hello
|
||||
+ Hello in div (at **)
|
||||
Good day
|
||||
- Bye
|
||||
+ Bye in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2678,13 +2624,18 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
it('fails if component stack is passed twice', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi %s%s', '\n in div', '\n in div');
|
||||
assertConsoleErrorDev(['Hi']);
|
||||
assertConsoleErrorDev(['Hi \n in div (at **)']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s""
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2693,16 +2644,23 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi %s%s', '\n in div', '\n in div');
|
||||
console.error('Bye %s%s', '\n in div', '\n in div');
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div (at **)',
|
||||
'Bye \n in div (at **)',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Hi %s%s"
|
||||
Unexpected error(s) recorded.
|
||||
|
||||
Received more than one component stack for a warning:
|
||||
"Bye %s%s""
|
||||
- Expected errors
|
||||
+ Received errors
|
||||
|
||||
Hi in div (at **)
|
||||
+ in div (at **)
|
||||
Bye in div (at **)
|
||||
+ in div (at **)"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -2711,14 +2669,14 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Hi \n in div');
|
||||
console.error('Bye \n in div');
|
||||
assertConsoleErrorDev('Hi', 'Bye');
|
||||
assertConsoleErrorDev('Hi \n in div', 'Bye \n in div');
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -2733,7 +2691,7 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Bye']);
|
||||
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
@@ -2749,7 +2707,133 @@ describe('ReactInternalTestUtils console assertions', () => {
|
||||
|
||||
Expected messages should be an array of strings but was given type "string"."
|
||||
`);
|
||||
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
|
||||
assertConsoleErrorDev([
|
||||
'Hi \n in div',
|
||||
'Wow \n in div',
|
||||
'Bye \n in div',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('in <stack> placeholder', () => {
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is used for a component stack instead of an error stack', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Warning message\n in div');
|
||||
assertConsoleErrorDev(['Warning message\n in <stack>']);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "Warning message
|
||||
in <stack>"
|
||||
Received: "Warning message
|
||||
in div (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
|
||||
`);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is used for multiple component stacks', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('First warning\n in span');
|
||||
console.error('Second warning\n in div');
|
||||
assertConsoleErrorDev([
|
||||
'First warning\n in <stack>',
|
||||
'Second warning\n in <stack>',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "First warning
|
||||
in <stack>"
|
||||
Received: "First warning
|
||||
in span (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)").
|
||||
|
||||
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
|
||||
|
||||
Expected: "Second warning
|
||||
in <stack>"
|
||||
Received: "Second warning
|
||||
in div (at **)"
|
||||
|
||||
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows `in <stack>` for actual error stack traces', () => {
|
||||
// This should pass - \n in <stack> is correctly used for an error stack
|
||||
console.error(new Error('Something went wrong'));
|
||||
assertConsoleErrorDev(['Error: Something went wrong\n in <stack>']);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if error stack trace is present but \\n in <stack> is not expected', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error(new Error('Something went wrong'));
|
||||
assertConsoleErrorDev(['Error: Something went wrong']);
|
||||
});
|
||||
expect(message).toMatch(`Unexpected error stack trace for:`);
|
||||
expect(message).toMatch(`Error: Something went wrong`);
|
||||
expect(message).toMatch(
|
||||
'If this error should include an error stack trace, add \\n in <stack> to your expected message'
|
||||
);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('fails if `in <stack>` is expected but no stack is present', () => {
|
||||
const message = expectToThrowFailure(() => {
|
||||
console.error('Error: Something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
'Error: Something went wrong\n in <stack>',
|
||||
]);
|
||||
});
|
||||
expect(message).toMatchInlineSnapshot(`
|
||||
"assertConsoleErrorDev(expected)
|
||||
|
||||
Missing error stack trace for:
|
||||
"Error: Something went wrong"
|
||||
|
||||
The expected message uses \\n in <stack> but the actual error doesn't include an error stack trace.
|
||||
If this error should not have an error stack trace, remove \\n in <stack> from your expected message."
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('[Environment] placeholder', () => {
|
||||
// @gate __DEV__
|
||||
it('expands [Server] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Server \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Server] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('expands [Prerender] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Prerender] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate __DEV__
|
||||
it('expands [Cache] to ANSI escape sequence for server badge', () => {
|
||||
const badge = '\u001b[0m\u001b[7m Cache \u001b[0m';
|
||||
console.error(badge + 'Error: something went wrong');
|
||||
assertConsoleErrorDev([
|
||||
['[Cache] Error: something went wrong', {withoutStack: true}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if waitFor is called before asserting', async () => {
|
||||
|
||||
@@ -168,6 +168,53 @@ function normalizeCodeLocInfo(str) {
|
||||
});
|
||||
}
|
||||
|
||||
// Expands environment placeholders like [Server] into ANSI escape sequences.
|
||||
// This allows test assertions to use a cleaner syntax like "[Server] Error:"
|
||||
// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"
|
||||
function expandEnvironmentPlaceholders(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
// [Environment] -> ANSI escape sequence for environment badge
|
||||
// The format is: reset + inverse + " Environment " + reset
|
||||
return str.replace(
|
||||
/^\[(\w+)] /g,
|
||||
(match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',
|
||||
);
|
||||
}
|
||||
|
||||
// The error stack placeholder that can be used in expected messages
|
||||
const ERROR_STACK_PLACEHOLDER = '\n in <stack>';
|
||||
// A marker used to protect the placeholder during normalization
|
||||
const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';
|
||||
|
||||
// Normalizes expected messages, handling special placeholders
|
||||
function normalizeExpectedMessage(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
// Protect the error stack placeholder from normalization
|
||||
// (normalizeCodeLocInfo would add "(at **)" to it)
|
||||
const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);
|
||||
let result = str;
|
||||
if (hasStackPlaceholder) {
|
||||
result = result.replace(
|
||||
ERROR_STACK_PLACEHOLDER,
|
||||
ERROR_STACK_PLACEHOLDER_MARKER,
|
||||
);
|
||||
}
|
||||
result = normalizeCodeLocInfo(result);
|
||||
result = expandEnvironmentPlaceholders(result);
|
||||
if (hasStackPlaceholder) {
|
||||
// Restore the placeholder (remove the "(at **)" that was added)
|
||||
result = result.replace(
|
||||
ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',
|
||||
ERROR_STACK_PLACEHOLDER,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeComponentStack(entry) {
|
||||
if (
|
||||
typeof entry[0] === 'string' &&
|
||||
@@ -187,6 +234,15 @@ const isLikelyAComponentStack = message =>
|
||||
message.includes('\n in ') ||
|
||||
message.includes('\n at '));
|
||||
|
||||
// Error stack traces start with "*Error:" and contain "at" frames with file paths
|
||||
// Component stacks contain "in ComponentName" patterns
|
||||
// This helps validate that \n in <stack> is used correctly
|
||||
const isLikelyAnErrorStackTrace = message =>
|
||||
typeof message === 'string' &&
|
||||
message.includes('Error:') &&
|
||||
// Has "at" frames typical of error stacks (with file:line:col)
|
||||
/\n\s+at .+\(.*:\d+:\d+\)/.test(message);
|
||||
|
||||
export function createLogAssertion(
|
||||
consoleMethod,
|
||||
matcherName,
|
||||
@@ -236,13 +292,11 @@ export function createLogAssertion(
|
||||
|
||||
const withoutStack = options.withoutStack;
|
||||
|
||||
// Warn about invalid global withoutStack values.
|
||||
if (consoleMethod === 'log' && withoutStack !== undefined) {
|
||||
throwFormattedError(
|
||||
`Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
|
||||
);
|
||||
} else if (withoutStack !== undefined && withoutStack !== true) {
|
||||
// withoutStack can only have a value true.
|
||||
throwFormattedError(
|
||||
`The second argument must be {withoutStack: true}.` +
|
||||
`\n\nInstead received ${JSON.stringify(options)}.`,
|
||||
@@ -256,8 +310,11 @@ export function createLogAssertion(
|
||||
const unexpectedLogs = [];
|
||||
const unexpectedMissingComponentStack = [];
|
||||
const unexpectedIncludingComponentStack = [];
|
||||
const unexpectedMissingErrorStack = [];
|
||||
const unexpectedIncludingErrorStack = [];
|
||||
const logsMismatchingFormat = [];
|
||||
const logsWithExtraComponentStack = [];
|
||||
const stackTracePlaceholderMisuses = [];
|
||||
|
||||
// Loop over all the observed logs to determine:
|
||||
// - Which expected logs are missing
|
||||
@@ -319,11 +376,11 @@ export function createLogAssertion(
|
||||
);
|
||||
}
|
||||
|
||||
expectedMessage = normalizeCodeLocInfo(currentExpectedMessage);
|
||||
expectedMessage = normalizeExpectedMessage(currentExpectedMessage);
|
||||
expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
|
||||
} else if (typeof expectedMessageOrArray === 'string') {
|
||||
// Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
|
||||
expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray);
|
||||
expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);
|
||||
// withoutStack: inherit from global option - simplify when withoutStack is removed.
|
||||
if (consoleMethod === 'log') {
|
||||
expectedWithoutStack = true;
|
||||
} else {
|
||||
@@ -381,19 +438,93 @@ export function createLogAssertion(
|
||||
}
|
||||
|
||||
// Main logic to check if log is expected, with the component stack.
|
||||
if (
|
||||
typeof expectedMessage === 'string' &&
|
||||
(normalizedMessage === expectedMessage ||
|
||||
normalizedMessage.includes(expectedMessage))
|
||||
) {
|
||||
// Check for exact match OR if the message matches with a component stack appended
|
||||
let matchesExpectedMessage = false;
|
||||
let expectsErrorStack = false;
|
||||
const hasErrorStack = isLikelyAnErrorStackTrace(message);
|
||||
|
||||
if (typeof expectedMessage === 'string') {
|
||||
if (normalizedMessage === expectedMessage) {
|
||||
matchesExpectedMessage = true;
|
||||
} else if (expectedMessage.includes('\n in <stack>')) {
|
||||
expectsErrorStack = true;
|
||||
// \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")
|
||||
// NOT for React component stacks (e.g., "\n in ComponentName (at **)").
|
||||
// Validate that the actual message looks like an error stack trace.
|
||||
if (!hasErrorStack) {
|
||||
// The actual message doesn't look like an error stack trace.
|
||||
// This is likely a misuse - someone used \n in <stack> for a component stack.
|
||||
stackTracePlaceholderMisuses.push({
|
||||
expected: expectedMessage,
|
||||
received: normalizedMessage,
|
||||
});
|
||||
}
|
||||
|
||||
const expectedMessageWithoutStack = expectedMessage.replace(
|
||||
'\n in <stack>',
|
||||
'',
|
||||
);
|
||||
if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {
|
||||
// Remove the stack trace
|
||||
const remainder = normalizedMessage.slice(
|
||||
expectedMessageWithoutStack.length,
|
||||
);
|
||||
|
||||
// After normalization, both error stacks and component stacks look like
|
||||
// component stacks (at frames are converted to "in ... (at **)" format).
|
||||
// So we check isLikelyAComponentStack for matching purposes.
|
||||
if (isLikelyAComponentStack(remainder)) {
|
||||
const messageWithoutStack = normalizedMessage.replace(
|
||||
remainder,
|
||||
'',
|
||||
);
|
||||
if (messageWithoutStack === expectedMessageWithoutStack) {
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (remainder === '') {
|
||||
// \n in <stack> was expected but there's no stack at all
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (normalizedMessage === expectedMessageWithoutStack) {
|
||||
// \n in <stack> was expected but actual has no stack at all (exact match without stack)
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
} else if (
|
||||
hasErrorStack &&
|
||||
!expectedMessage.includes('\n in <stack>') &&
|
||||
normalizedMessage.startsWith(expectedMessage)
|
||||
) {
|
||||
matchesExpectedMessage = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesExpectedMessage) {
|
||||
// withoutStack: Check for unexpected/missing component stacks.
|
||||
// These checks can be simplified when withoutStack is removed.
|
||||
if (isLikelyAComponentStack(normalizedMessage)) {
|
||||
if (expectedWithoutStack === true) {
|
||||
if (expectedWithoutStack === true && !hasErrorStack) {
|
||||
// Only report unexpected component stack if it's not an error stack
|
||||
// (error stacks look like component stacks after normalization)
|
||||
unexpectedIncludingComponentStack.push(normalizedMessage);
|
||||
}
|
||||
} else if (expectedWithoutStack !== true) {
|
||||
} else if (expectedWithoutStack !== true && !expectsErrorStack) {
|
||||
unexpectedMissingComponentStack.push(normalizedMessage);
|
||||
}
|
||||
|
||||
// Check for unexpected/missing error stacks
|
||||
if (hasErrorStack && !expectsErrorStack) {
|
||||
// Error stack is present but \n in <stack> was not in the expected message
|
||||
unexpectedIncludingErrorStack.push(normalizedMessage);
|
||||
} else if (
|
||||
expectsErrorStack &&
|
||||
!hasErrorStack &&
|
||||
!isLikelyAComponentStack(normalizedMessage)
|
||||
) {
|
||||
// \n in <stack> was expected but the actual message doesn't have any stack at all
|
||||
// (if it has a component stack, stackTracePlaceholderMisuses already handles it)
|
||||
unexpectedMissingErrorStack.push(normalizedMessage);
|
||||
}
|
||||
|
||||
// Found expected log, remove it from missing.
|
||||
missingExpectedLogs.splice(0, 1);
|
||||
} else {
|
||||
@@ -422,6 +553,21 @@ export function createLogAssertion(
|
||||
)}`;
|
||||
}
|
||||
|
||||
// Wrong %s formatting is a failure.
|
||||
// This is a common mistake when creating new warnings.
|
||||
if (logsMismatchingFormat.length > 0) {
|
||||
throwFormattedError(
|
||||
logsMismatchingFormat
|
||||
.map(
|
||||
item =>
|
||||
`Received ${item.args.length} arguments for a message with ${
|
||||
item.expectedArgCount
|
||||
} placeholders:\n ${printReceived(item.format)}`,
|
||||
)
|
||||
.join('\n\n'),
|
||||
);
|
||||
}
|
||||
|
||||
// Any unexpected warnings should be treated as a failure.
|
||||
if (unexpectedLogs.length > 0) {
|
||||
throwFormattedError(
|
||||
@@ -466,18 +612,33 @@ export function createLogAssertion(
|
||||
);
|
||||
}
|
||||
|
||||
// Wrong %s formatting is a failure.
|
||||
// This is a common mistake when creating new warnings.
|
||||
if (logsMismatchingFormat.length > 0) {
|
||||
// Any logs that include an error stack trace but \n in <stack> wasn't expected.
|
||||
if (unexpectedIncludingErrorStack.length > 0) {
|
||||
throwFormattedError(
|
||||
logsMismatchingFormat
|
||||
`${unexpectedIncludingErrorStack
|
||||
.map(
|
||||
item =>
|
||||
`Received ${item.args.length} arguments for a message with ${
|
||||
item.expectedArgCount
|
||||
} placeholders:\n ${printReceived(item.format)}`,
|
||||
stack =>
|
||||
`Unexpected error stack trace for:\n ${printReceived(stack)}`,
|
||||
)
|
||||
.join('\n\n'),
|
||||
.join(
|
||||
'\n\n',
|
||||
)}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +
|
||||
`(e.g., "Error: message\\n in <stack>").`,
|
||||
);
|
||||
}
|
||||
|
||||
// Any logs that are missing an error stack trace when \n in <stack> was expected.
|
||||
if (unexpectedMissingErrorStack.length > 0) {
|
||||
throwFormattedError(
|
||||
`${unexpectedMissingErrorStack
|
||||
.map(
|
||||
stack =>
|
||||
`Missing error stack trace for:\n ${printReceived(stack)}`,
|
||||
)
|
||||
.join(
|
||||
'\n\n',
|
||||
)}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +
|
||||
`\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -496,6 +657,25 @@ export function createLogAssertion(
|
||||
.join('\n\n'),
|
||||
);
|
||||
}
|
||||
|
||||
// Using \n in <stack> for component stacks is a misuse.
|
||||
// \n in <stack> should only be used for JavaScript Error stack traces,
|
||||
// not for React component stacks.
|
||||
if (stackTracePlaceholderMisuses.length > 0) {
|
||||
throwFormattedError(
|
||||
`${stackTracePlaceholderMisuses
|
||||
.map(
|
||||
item =>
|
||||
`Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +
|
||||
`stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +
|
||||
`Expected: ${printReceived(item.expected)}\n` +
|
||||
`Received: ${printReceived(item.received)}\n\n` +
|
||||
`If this ${logName()} has a component stack, include the full component stack in your expected message ` +
|
||||
`(e.g., "Warning message\\n in ComponentName (at **)").`,
|
||||
)
|
||||
.join('\n\n')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +79,18 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
|
||||
status: promise.status,
|
||||
};
|
||||
}
|
||||
} else if ('value' in ioInfo) {
|
||||
// If value exists in ioInfo but is undefined (e.g., WeakRef was GC'd),
|
||||
// ensure we still include it in the normalized output for consistency
|
||||
copy.value = {
|
||||
value: undefined,
|
||||
};
|
||||
} else if (ioInfo.name && ioInfo.name !== 'rsc stream') {
|
||||
// For non-rsc-stream IO that doesn't have a value field, add a default.
|
||||
// This handles the case where the server doesn't send the field when WeakRef is GC'd.
|
||||
copy.value = {
|
||||
value: undefined,
|
||||
};
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -1515,16 +1515,13 @@ describe('ReactFlight', () => {
|
||||
},
|
||||
};
|
||||
const transport = ReactNoopFlightServer.render(<input value={obj} />);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <input value={{toJSON: ...}}>\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <input value={{toJSON: ...}}>\n' +
|
||||
' ^^^^^^^^^^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1546,14 +1543,11 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<div>Womp womp: {new MyError('spaghetti')}</div>,
|
||||
);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Womp womp: {Error}</div>\n' +
|
||||
' ^^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Womp womp: {Error}</div>\n' +
|
||||
' ^^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1566,15 +1560,12 @@ describe('ReactFlight', () => {
|
||||
|
||||
it('should warn in DEV if a special object is passed to a host component', () => {
|
||||
const transport = ReactNoopFlightServer.render(<input value={Math} />);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Math objects are not supported.\n' +
|
||||
' <input value={Math}>\n' +
|
||||
' ^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Math objects are not supported.\n' +
|
||||
' <input value={Math}>\n' +
|
||||
' ^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1590,15 +1581,12 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<input value={{[Symbol.iterator]: {}}} />,
|
||||
);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <input value={{}}>\n' +
|
||||
' ^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <input value={{}}>\n' +
|
||||
' ^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1621,16 +1609,13 @@ describe('ReactFlight', () => {
|
||||
}
|
||||
const Client = clientReference(ClientImpl);
|
||||
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <... value={{toJSON: ...}}>\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <... value={{toJSON: ...}}>\n' +
|
||||
' ^^^^^^^^^^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1656,16 +1641,13 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<Client>Current date: {obj}</Client>,
|
||||
);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <>Current date: {{toJSON: ...}}</>\n' +
|
||||
' ^^^^^^^^^^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <>Current date: {{toJSON: ...}}</>\n' +
|
||||
' ^^^^^^^^^^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1684,15 +1666,12 @@ describe('ReactFlight', () => {
|
||||
}
|
||||
const Client = clientReference(ClientImpl);
|
||||
const transport = ReactNoopFlightServer.render(<Client value={Math} />);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Math objects are not supported.\n' +
|
||||
' <... value={Math}>\n' +
|
||||
' ^^^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Math objects are not supported.\n' +
|
||||
' <... value={Math}>\n' +
|
||||
' ^^^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
assertConsoleErrorDev([
|
||||
@@ -1713,15 +1692,12 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<Client value={{[Symbol.iterator]: {}}} />,
|
||||
);
|
||||
assertConsoleErrorDev(
|
||||
[
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <... value={{}}>\n' +
|
||||
' ^^^^',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
assertConsoleErrorDev([
|
||||
['Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <... value={{}}>\n' +
|
||||
' ^^^^', {withoutStack: true}],
|
||||
]);
|
||||
|
||||
ReactNoopFlightClient.read(transport);
|
||||
|
||||
@@ -1729,7 +1705,8 @@ describe('ReactFlight', () => {
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
|
||||
' <... value={{}}>\n' +
|
||||
' ^^^^\n',
|
||||
' ^^^^\n' +
|
||||
' in (at **)',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3258,7 +3235,7 @@ describe('ReactFlight', () => {
|
||||
const transport = ReactNoopFlightServer.render({
|
||||
root: ReactServer.createElement(App),
|
||||
});
|
||||
assertConsoleErrorDev(['Error: err']);
|
||||
assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
|
||||
|
||||
@@ -467,9 +467,11 @@ function useSyncExternalStore<T>(
|
||||
// useSyncExternalStore() composes multiple hooks internally.
|
||||
// Advance the current hook index the same number of times
|
||||
// so that subsequent hooks have the right memoized state.
|
||||
nextHook(); // SyncExternalStore
|
||||
const hook = nextHook(); // SyncExternalStore
|
||||
nextHook(); // Effect
|
||||
const value = getSnapshot();
|
||||
// Read from hook.memoizedState to get the value that was used during render,
|
||||
// not the current value from getSnapshot() which may have changed.
|
||||
const value = hook !== null ? hook.memoizedState : getSnapshot();
|
||||
hookLog.push({
|
||||
displayName: null,
|
||||
primitive: 'SyncExternalStore',
|
||||
|
||||
@@ -734,7 +734,11 @@ describe('ReactHooksInspection', () => {
|
||||
});
|
||||
const results = normalizeSourceLoc(tree);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]).toMatchInlineSnapshot(`
|
||||
expect(results[0]).toMatchInlineSnapshot(
|
||||
{
|
||||
subHooks: [{value: expect.any(Promise)}],
|
||||
},
|
||||
`
|
||||
{
|
||||
"debugInfo": null,
|
||||
"hookSource": {
|
||||
@@ -759,12 +763,13 @@ describe('ReactHooksInspection', () => {
|
||||
"isStateEditable": false,
|
||||
"name": "Use",
|
||||
"subHooks": [],
|
||||
"value": Promise {},
|
||||
"value": Any<Promise>,
|
||||
},
|
||||
],
|
||||
"value": undefined,
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('useDebugValue', () => {
|
||||
|
||||
@@ -17,6 +17,14 @@ const contentScriptsToInject = [
|
||||
runAt: 'document_end',
|
||||
world: chrome.scripting.ExecutionWorld.ISOLATED,
|
||||
},
|
||||
{
|
||||
id: '@react-devtools/fallback-eval-context',
|
||||
js: ['build/fallbackEvalContext.js'],
|
||||
matches: ['<all_urls>'],
|
||||
persistAcrossSessions: true,
|
||||
runAt: 'document_start',
|
||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||
},
|
||||
{
|
||||
id: '@react-devtools/hook',
|
||||
js: ['build/installHook.js'],
|
||||
|
||||
@@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'eval-in-inspected-window': {
|
||||
const {
|
||||
payload: {tabId, requestId, scriptId, args},
|
||||
} = message;
|
||||
|
||||
chrome.tabs
|
||||
.sendMessage(tabId, {
|
||||
source: 'devtools-page-eval',
|
||||
payload: {
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response) {
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result: null,
|
||||
error: 'No response from content script',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const {result, error} = response;
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result,
|
||||
error,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'react-devtools-background',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window-response',
|
||||
requestId,
|
||||
result: null,
|
||||
error: error?.message || String(error),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js
vendored
Normal file
35
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {evalScripts} from '../evalScripts';
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data?.source === 'react-devtools-content-script-eval') {
|
||||
const {scriptId, args, requestId} = event.data.payload;
|
||||
const response = {result: null, error: null};
|
||||
try {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
response.result = evalScripts[scriptId].fn.apply(null, args);
|
||||
} catch (err) {
|
||||
response.error = err.message;
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script-eval-response',
|
||||
payload: {
|
||||
requestId,
|
||||
response,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -117,3 +117,49 @@ function connectPort() {
|
||||
// $FlowFixMe[incompatible-use]
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
}
|
||||
|
||||
let evalRequestId = 0;
|
||||
const evalRequestCallbacks = new Map<number, Function>();
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
switch (msg?.source) {
|
||||
case 'devtools-page-eval': {
|
||||
const {scriptId, args} = msg.payload;
|
||||
const requestId = evalRequestId++;
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'react-devtools-content-script-eval',
|
||||
payload: {
|
||||
requestId,
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
evalRequestCallbacks.set(requestId, sendResponse);
|
||||
return true; // Indicate we will respond asynchronously
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
if (event.data?.source === 'react-devtools-content-script-eval-response') {
|
||||
const {requestId, response} = event.data.payload;
|
||||
const callback = evalRequestCallbacks.get(requestId);
|
||||
try {
|
||||
if (!callback)
|
||||
throw new Error(
|
||||
`No eval request callback for id "${requestId}" exists.`,
|
||||
);
|
||||
callback(response);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'React DevTools Content Script eval response error occurred:',
|
||||
e,
|
||||
);
|
||||
} finally {
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
112
packages/react-devtools-extensions/src/evalScripts.js
vendored
Normal file
112
packages/react-devtools-extensions/src/evalScripts.js
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export type EvalScriptIds =
|
||||
| 'checkIfReactPresentInInspectedWindow'
|
||||
| 'reload'
|
||||
| 'setBrowserSelectionFromReact'
|
||||
| 'setReactSelectionFromBrowser'
|
||||
| 'viewAttributeSource'
|
||||
| 'viewElementSource';
|
||||
|
||||
/*
|
||||
.fn for fallback in Content Script context
|
||||
.code for chrome.devtools.inspectedWindow.eval()
|
||||
*/
|
||||
type EvalScriptEntry = {
|
||||
fn: (...args: any[]) => any,
|
||||
code: (...args: any[]) => string,
|
||||
};
|
||||
|
||||
/*
|
||||
Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
|
||||
So some fallback functions are no-op or throw error.
|
||||
*/
|
||||
export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
|
||||
checkIfReactPresentInInspectedWindow: {
|
||||
fn: () =>
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
|
||||
code: () =>
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
|
||||
},
|
||||
reload: {
|
||||
fn: () => window.location.reload(),
|
||||
code: () => 'window.location.reload();',
|
||||
},
|
||||
setBrowserSelectionFromReact: {
|
||||
fn: () => {
|
||||
throw new Error('Not supported in fallback eval context');
|
||||
},
|
||||
code: () =>
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
|
||||
'false',
|
||||
},
|
||||
setReactSelectionFromBrowser: {
|
||||
fn: () => {
|
||||
throw new Error('Not supported in fallback eval context');
|
||||
},
|
||||
code: () =>
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
|
||||
'false',
|
||||
},
|
||||
viewAttributeSource: {
|
||||
fn: ({rendererID, elementID, path}) => {
|
||||
return false; // Not supported in fallback eval context
|
||||
},
|
||||
code: ({rendererID, elementID, path}) =>
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementAttributeByPath(' +
|
||||
JSON.stringify(elementID) +
|
||||
',' +
|
||||
JSON.stringify(path) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
},
|
||||
viewElementSource: {
|
||||
fn: ({rendererID, elementID}) => {
|
||||
return false; // Not supported in fallback eval context
|
||||
},
|
||||
code: ({rendererID, elementID}) =>
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementSourceFunctionById(' +
|
||||
JSON.stringify(elementID) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
},
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
/* global chrome */
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
|
||||
export function setBrowserSelectionFromReact() {
|
||||
// This is currently only called on demand when you press "view DOM".
|
||||
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
|
||||
// we could make this happen automatically when you select another component.
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
|
||||
'false',
|
||||
evalInInspectedWindow(
|
||||
'setBrowserSelectionFromReact',
|
||||
[],
|
||||
(didSelectionChange, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
@@ -19,10 +18,9 @@ export function setBrowserSelectionFromReact() {
|
||||
export function setReactSelectionFromBrowser(bridge) {
|
||||
// When the user chooses a different node in the browser Elements tab,
|
||||
// copy it over to the hook object so that we can sync the selection.
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
||||
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
|
||||
'false',
|
||||
evalInInspectedWindow(
|
||||
'setReactSelectionFromBrowser',
|
||||
[],
|
||||
(didSelectionChange, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
@@ -34,7 +32,7 @@ export function setReactSelectionFromBrowser(bridge) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remember to sync the selection next time we show Components tab.
|
||||
// Remember to sync the selection next time we show inspected element
|
||||
bridge.send('syncSelectionFromBuiltinElementsPanel');
|
||||
}
|
||||
},
|
||||
|
||||
116
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js
vendored
Normal file
116
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {EvalScriptIds} from '../evalScripts';
|
||||
|
||||
import {evalScripts} from '../evalScripts';
|
||||
|
||||
type ExceptionInfo = {
|
||||
code: ?string,
|
||||
description: ?string,
|
||||
isError: boolean,
|
||||
isException: boolean,
|
||||
value: any,
|
||||
};
|
||||
|
||||
const EVAL_TIMEOUT = 1000 * 10;
|
||||
|
||||
let evalRequestId = 0;
|
||||
const evalRequestCallbacks = new Map<
|
||||
number,
|
||||
(value: {result: any, error: any}) => void,
|
||||
>();
|
||||
|
||||
function fallbackEvalInInspectedWindow(
|
||||
scriptId: EvalScriptIds,
|
||||
args: any[],
|
||||
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
|
||||
) {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
const code = evalScripts[scriptId].code.apply(null, args);
|
||||
const tabId = chrome.devtools.inspectedWindow.tabId;
|
||||
const requestId = evalRequestId++;
|
||||
chrome.runtime.sendMessage({
|
||||
source: 'devtools-page',
|
||||
payload: {
|
||||
type: 'eval-in-inspected-window',
|
||||
tabId,
|
||||
requestId,
|
||||
scriptId,
|
||||
args,
|
||||
},
|
||||
});
|
||||
const timeout = setTimeout(() => {
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
if (callback) {
|
||||
callback(null, {
|
||||
code,
|
||||
description:
|
||||
'Timed out while waiting for eval response from the inspected window.',
|
||||
isError: true,
|
||||
isException: false,
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
}, EVAL_TIMEOUT);
|
||||
evalRequestCallbacks.set(requestId, ({result, error}) => {
|
||||
clearTimeout(timeout);
|
||||
evalRequestCallbacks.delete(requestId);
|
||||
if (callback) {
|
||||
if (error) {
|
||||
callback(null, {
|
||||
code,
|
||||
description: undefined,
|
||||
isError: false,
|
||||
isException: true,
|
||||
value: error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
callback(result, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function evalInInspectedWindow(
|
||||
scriptId: EvalScriptIds,
|
||||
args: any[],
|
||||
callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
|
||||
) {
|
||||
if (!evalScripts[scriptId]) {
|
||||
throw new Error(`No eval script with id "${scriptId}" exists.`);
|
||||
}
|
||||
const code = evalScripts[scriptId].code.apply(null, args);
|
||||
chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
|
||||
if (!exceptionInfo) {
|
||||
callback(result, exceptionInfo);
|
||||
return;
|
||||
}
|
||||
// If an exception (e.g. CSP Blocked) occurred,
|
||||
// fallback to the content script eval context
|
||||
fallbackEvalInInspectedWindow(scriptId, args, callback);
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(({payload, source}) => {
|
||||
if (source === 'react-devtools-background') {
|
||||
switch (payload?.type) {
|
||||
case 'eval-in-inspected-window-response': {
|
||||
const {requestId, result, error} = payload;
|
||||
const callback = evalRequestCallbacks.get(requestId);
|
||||
if (callback) {
|
||||
callback({result, error});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
139
packages/react-devtools-extensions/src/main/index.js
vendored
139
packages/react-devtools-extensions/src/main/index.js
vendored
@@ -1,6 +1,14 @@
|
||||
/* global chrome */
|
||||
/** @flow */
|
||||
|
||||
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
|
||||
import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
|
||||
import type {
|
||||
TabID,
|
||||
ViewElementSource,
|
||||
} from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||
import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import {createElement} from 'react';
|
||||
import {flushSync} from 'react-dom';
|
||||
@@ -32,6 +40,7 @@ import {
|
||||
} from './elementSelection';
|
||||
import {viewAttributeSource} from './sourceSelection';
|
||||
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
import {startReactPolling} from './reactPolling';
|
||||
import {cloneStyleTags} from './cloneStyleTags';
|
||||
import fetchFileWithCaching from './fetchFileWithCaching';
|
||||
@@ -50,9 +59,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
|
||||
function createBridge() {
|
||||
bridge = new Bridge({
|
||||
listen(fn) {
|
||||
const bridgeListener = message => fn(message);
|
||||
const bridgeListener = (message: Message) => fn(message);
|
||||
// Store the reference so that we unsubscribe from the same object.
|
||||
const portOnMessage = port.onMessage;
|
||||
const portOnMessage = ((port: any): ExtensionPort).onMessage;
|
||||
portOnMessage.addListener(bridgeListener);
|
||||
|
||||
lastSubscribedBridgeListener = bridgeListener;
|
||||
@@ -70,7 +79,7 @@ function createBridge() {
|
||||
|
||||
bridge.addListener('reloadAppForProfiling', () => {
|
||||
localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
|
||||
chrome.devtools.inspectedWindow.eval('window.location.reload();');
|
||||
evalInInspectedWindow('reload', [], () => {});
|
||||
});
|
||||
|
||||
bridge.addListener(
|
||||
@@ -175,14 +184,20 @@ function createBridgeAndStore() {
|
||||
// Otherwise, the Store may miss important initial tree op codes.
|
||||
injectBackendManager(chrome.devtools.inspectedWindow.tabId);
|
||||
|
||||
const viewAttributeSourceFunction = (id, path) => {
|
||||
const viewAttributeSourceFunction = (
|
||||
id: Element['id'],
|
||||
path: Array<string | number>,
|
||||
) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
viewAttributeSource(rendererID, id, path);
|
||||
}
|
||||
};
|
||||
|
||||
const viewElementSourceFunction = (source, symbolicatedSource) => {
|
||||
const viewElementSourceFunction: ViewElementSource = (
|
||||
source,
|
||||
symbolicatedSource,
|
||||
) => {
|
||||
const [, sourceURL, line, column] = symbolicatedSource
|
||||
? symbolicatedSource
|
||||
: source;
|
||||
@@ -197,7 +212,7 @@ function createBridgeAndStore() {
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
render = (overrideTab = mostRecentOverrideTab) => {
|
||||
render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
|
||||
mostRecentOverrideTab = overrideTab;
|
||||
|
||||
root.render(
|
||||
@@ -205,6 +220,7 @@ function createBridgeAndStore() {
|
||||
bridge,
|
||||
browserTheme: getBrowserTheme(),
|
||||
componentsPortalContainer,
|
||||
inspectedElementPortalContainer,
|
||||
profilerPortalContainer,
|
||||
editorPortalContainer,
|
||||
currentSelectedSource,
|
||||
@@ -225,7 +241,9 @@ function createBridgeAndStore() {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureInitialHTMLIsCleared(container) {
|
||||
function ensureInitialHTMLIsCleared(
|
||||
container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
|
||||
) {
|
||||
if (container._hasInitialHTMLBeenCleared) {
|
||||
return;
|
||||
}
|
||||
@@ -277,6 +295,52 @@ function createComponentsPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
function createElementsInspectPanel() {
|
||||
if (inspectedElementPortalContainer) {
|
||||
// Panel is created and user opened it at least once
|
||||
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
|
||||
render();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (inspectedElementPane) {
|
||||
// Panel is created, but wasn't opened yet, so no document is present for it
|
||||
return;
|
||||
}
|
||||
|
||||
const elementsPanel = chrome.devtools.panels.elements;
|
||||
if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
|
||||
// Firefox will not pass the window to the onShown listener despite setPage
|
||||
// being called.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=2010549
|
||||
|
||||
// May not be supported in some browsers.
|
||||
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels/ElementsPanel/createSidebarPane#browser_compatibility
|
||||
return;
|
||||
}
|
||||
|
||||
elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
|
||||
inspectedElementPane = createdPane;
|
||||
|
||||
createdPane.setPage('panel.html');
|
||||
createdPane.setHeight('75px');
|
||||
|
||||
createdPane.onShown.addListener(portal => {
|
||||
inspectedElementPortalContainer = portal.container;
|
||||
if (inspectedElementPortalContainer != null && render) {
|
||||
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
|
||||
bridge.send('syncSelectionFromBuiltinElementsPanel');
|
||||
|
||||
render();
|
||||
portal.injectStyles(cloneStyleTags);
|
||||
|
||||
logEvent({event_name: 'selected-inspected-element-pane'});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createProfilerPanel() {
|
||||
if (profilerPortalContainer) {
|
||||
// Panel is created and user opened it at least once
|
||||
@@ -350,13 +414,6 @@ function createSourcesEditorPanel() {
|
||||
logEvent({event_name: 'selected-editor-pane'});
|
||||
}
|
||||
});
|
||||
|
||||
createdPane.onShown.addListener(() => {
|
||||
bridge.emit('extensionEditorPaneShown');
|
||||
});
|
||||
createdPane.onHidden.addListener(() => {
|
||||
bridge.emit('extensionEditorPaneHidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,10 +489,10 @@ function performInTabNavigationCleanup() {
|
||||
// Do not clean mostRecentOverrideTab on purpose, so we remember last opened
|
||||
// React DevTools tab, when user does in-tab navigation
|
||||
|
||||
store = null;
|
||||
bridge = null;
|
||||
render = null;
|
||||
root = null;
|
||||
store = (null: $FlowFixMe);
|
||||
bridge = (null: $FlowFixMe);
|
||||
render = (null: $FlowFixMe);
|
||||
root = (null: $FlowFixMe);
|
||||
}
|
||||
|
||||
function performFullCleanup() {
|
||||
@@ -457,18 +514,18 @@ function performFullCleanup() {
|
||||
componentsPortalContainer = null;
|
||||
profilerPortalContainer = null;
|
||||
suspensePortalContainer = null;
|
||||
root = null;
|
||||
root = (null: $FlowFixMe);
|
||||
|
||||
mostRecentOverrideTab = null;
|
||||
store = null;
|
||||
bridge = null;
|
||||
render = null;
|
||||
store = (null: $FlowFixMe);
|
||||
bridge = (null: $FlowFixMe);
|
||||
render = (null: $FlowFixMe);
|
||||
|
||||
port?.disconnect();
|
||||
port = null;
|
||||
port = (null: $FlowFixMe);
|
||||
}
|
||||
|
||||
function connectExtensionPort() {
|
||||
function connectExtensionPort(): void {
|
||||
if (port) {
|
||||
throw new Error('DevTools port was already connected');
|
||||
}
|
||||
@@ -492,7 +549,7 @@ function connectExtensionPort() {
|
||||
// so, when we call `port.disconnect()` from this script,
|
||||
// this should not trigger this callback and port reconnection
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
port = (null: $FlowFixMe);
|
||||
connectExtensionPort();
|
||||
});
|
||||
}
|
||||
@@ -507,6 +564,7 @@ function mountReactDevTools() {
|
||||
createComponentsPanel();
|
||||
createProfilerPanel();
|
||||
createSourcesEditorPanel();
|
||||
createElementsInspectPanel();
|
||||
// Suspense Tab is created via the hook
|
||||
// TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable
|
||||
}
|
||||
@@ -545,9 +603,9 @@ function mountReactDevToolsWhenReactHasLoaded() {
|
||||
);
|
||||
}
|
||||
|
||||
let bridge = null;
|
||||
let bridge: FrontendBridge = (null: $FlowFixMe);
|
||||
let lastSubscribedBridgeListener = null;
|
||||
let store = null;
|
||||
let store: Store = (null: $FlowFixMe);
|
||||
|
||||
let profilingData = null;
|
||||
|
||||
@@ -555,18 +613,35 @@ let componentsPanel = null;
|
||||
let profilerPanel = null;
|
||||
let suspensePanel = null;
|
||||
let editorPane = null;
|
||||
let inspectedElementPane = null;
|
||||
let componentsPortalContainer = null;
|
||||
let profilerPortalContainer = null;
|
||||
let suspensePortalContainer = null;
|
||||
let editorPortalContainer = null;
|
||||
let inspectedElementPortalContainer = null;
|
||||
|
||||
let mostRecentOverrideTab = null;
|
||||
let render = null;
|
||||
let root = null;
|
||||
let mostRecentOverrideTab: null | TabID = null;
|
||||
let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
|
||||
let root: RootType = (null: $FlowFixMe);
|
||||
|
||||
let currentSelectedSource: null | SourceSelection = null;
|
||||
|
||||
let port = null;
|
||||
type ExtensionEvent = {
|
||||
addListener(callback: (message: Message, port: ExtensionPort) => void): void,
|
||||
removeListener(
|
||||
callback: (message: Message, port: ExtensionPort) => void,
|
||||
): void,
|
||||
};
|
||||
|
||||
/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
|
||||
type ExtensionPort = {
|
||||
onDisconnect: ExtensionEvent,
|
||||
onMessage: ExtensionEvent,
|
||||
postMessage(message: mixed, transferable?: Array<mixed>): void,
|
||||
disconnect(): void,
|
||||
};
|
||||
|
||||
let port: ExtensionPort = (null: $FlowFixMe);
|
||||
|
||||
// In case when multiple navigation events emitted in a short period of time
|
||||
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
|
||||
@@ -599,7 +674,7 @@ connectExtensionPort();
|
||||
|
||||
mountReactDevToolsWhenReactHasLoaded();
|
||||
|
||||
function onThemeChanged(themeName) {
|
||||
function onThemeChanged() {
|
||||
// Rerender with the new theme
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global chrome */
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
|
||||
class CouldNotFindReactOnThePageError extends Error {
|
||||
constructor() {
|
||||
@@ -26,8 +26,9 @@ export function startReactPolling(
|
||||
|
||||
// This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
|
||||
function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
|
||||
evalInInspectedWindow(
|
||||
'checkIfReactPresentInInspectedWindow',
|
||||
[],
|
||||
(pageHasReact, exceptionInfo) => {
|
||||
if (status === 'aborted') {
|
||||
onError(
|
||||
|
||||
@@ -1,27 +1,9 @@
|
||||
/* global chrome */
|
||||
import {evalInInspectedWindow} from './evalInInspectedWindow';
|
||||
|
||||
export function viewAttributeSource(rendererID, elementID, path) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementAttributeByPath(' +
|
||||
JSON.stringify(elementID) +
|
||||
',' +
|
||||
JSON.stringify(path) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
evalInInspectedWindow(
|
||||
'viewAttributeSource',
|
||||
[{rendererID, elementID, path}],
|
||||
(didInspect, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
@@ -31,25 +13,9 @@ export function viewAttributeSource(rendererID, elementID, path) {
|
||||
}
|
||||
|
||||
export function viewElementSource(rendererID, elementID) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'{' + // The outer block is important because it means we can declare local variables.
|
||||
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
|
||||
JSON.stringify(rendererID) +
|
||||
');' +
|
||||
'if (renderer) {' +
|
||||
' const value = renderer.getElementSourceFunctionById(' +
|
||||
JSON.stringify(elementID) +
|
||||
');' +
|
||||
' if (value) {' +
|
||||
' inspect(value);' +
|
||||
' true;' +
|
||||
' } else {' +
|
||||
' false;' +
|
||||
' }' +
|
||||
'} else {' +
|
||||
' false;' +
|
||||
'}' +
|
||||
'}',
|
||||
evalInInspectedWindow(
|
||||
'viewElementSource',
|
||||
[{rendererID, elementID}],
|
||||
(didInspect, evalError) => {
|
||||
if (evalError) {
|
||||
console.error(evalError);
|
||||
|
||||
@@ -69,6 +69,7 @@ module.exports = {
|
||||
backend: './src/backend.js',
|
||||
background: './src/background/index.js',
|
||||
backendManager: './src/contentScripts/backendManager.js',
|
||||
fallbackEvalContext: './src/contentScripts/fallbackEvalContext.js',
|
||||
fileFetcher: './src/contentScripts/fileFetcher.js',
|
||||
main: './src/main/index.js',
|
||||
panel: './src/panel.js',
|
||||
|
||||
34
packages/react-devtools-inline/src/backend.js
vendored
34
packages/react-devtools-inline/src/backend.js
vendored
@@ -6,7 +6,10 @@ import {initBackend} from 'react-devtools-shared/src/backend';
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {
|
||||
BackendBridge,
|
||||
SavedPreferencesParams,
|
||||
} from 'react-devtools-shared/src/bridge';
|
||||
import type {Wall} from 'react-devtools-shared/src/frontend/types';
|
||||
import {
|
||||
getIfReloadedAndProfiling,
|
||||
@@ -16,31 +19,14 @@ import {
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
|
||||
function startActivation(contentWindow: any, bridge: BackendBridge) {
|
||||
const onSavedPreferences = (data: $FlowFixMe) => {
|
||||
const onSavedPreferences = (data: SavedPreferencesParams) => {
|
||||
// This is the only message we're listening for,
|
||||
// so it's safe to cleanup after we've received it.
|
||||
bridge.removeListener('savedPreferences', onSavedPreferences);
|
||||
|
||||
const {
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
componentFilters,
|
||||
showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode,
|
||||
disableSecondConsoleLogDimmingInStrictMode,
|
||||
} = data;
|
||||
const {componentFilters} = data;
|
||||
|
||||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ =
|
||||
appendComponentStack;
|
||||
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ =
|
||||
breakOnConsoleErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
|
||||
showInlineWarningsAndErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
|
||||
hideConsoleLogsInStrictMode;
|
||||
contentWindow.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ =
|
||||
disableSecondConsoleLogDimmingInStrictMode;
|
||||
|
||||
// TRICKY
|
||||
// The backend entry point may be required in the context of an iframe or the parent window.
|
||||
@@ -49,15 +35,7 @@ function startActivation(contentWindow: any, bridge: BackendBridge) {
|
||||
// Technically we don't need to store them on the contentWindow in this case,
|
||||
// but it doesn't really hurt anything to store them there too.
|
||||
if (contentWindow !== window) {
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
|
||||
showInlineWarningsAndErrors;
|
||||
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
|
||||
hideConsoleLogsInStrictMode;
|
||||
window.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ =
|
||||
disableSecondConsoleLogDimmingInStrictMode;
|
||||
}
|
||||
|
||||
finishActivation(contentWindow, bridge);
|
||||
|
||||
6
packages/react-devtools-shared/src/Logger.js
vendored
6
packages/react-devtools-shared/src/Logger.js
vendored
@@ -63,7 +63,11 @@ export type LoggerEvent =
|
||||
+value: any,
|
||||
...
|
||||
},
|
||||
};
|
||||
}
|
||||
| {
|
||||
+event_name: 'selected-editor-pane',
|
||||
}
|
||||
| {+event_name: 'selected-inspected-element-pane'};
|
||||
|
||||
export type LogFunction = LoggerEvent => void | Promise<void>;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('Profiler change descriptions', () => {
|
||||
{
|
||||
"context": true,
|
||||
"didHooksChange": false,
|
||||
"hooks": null,
|
||||
"hooks": [],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
@@ -110,7 +110,7 @@ describe('Profiler change descriptions', () => {
|
||||
{
|
||||
"context": true,
|
||||
"didHooksChange": false,
|
||||
"hooks": null,
|
||||
"hooks": [],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
@@ -125,7 +125,7 @@ describe('Profiler change descriptions', () => {
|
||||
{
|
||||
"context": false,
|
||||
"didHooksChange": false,
|
||||
"hooks": null,
|
||||
"hooks": [],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
@@ -140,7 +140,7 @@ describe('Profiler change descriptions', () => {
|
||||
{
|
||||
"context": true,
|
||||
"didHooksChange": false,
|
||||
"hooks": null,
|
||||
"hooks": [],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
|
||||
@@ -378,6 +378,12 @@ describe('ProfilingCache', () => {
|
||||
),
|
||||
);
|
||||
|
||||
// Save references to the real dispatch/setState functions.
|
||||
// inspectHooks() re-runs the component with a mock dispatcher,
|
||||
// which would overwrite these variables with mock functions that do nothing.
|
||||
const realDispatch = dispatch;
|
||||
const realSetState = setState;
|
||||
|
||||
// Second render has no changed hooks, only changed props.
|
||||
utils.act(() =>
|
||||
render(
|
||||
@@ -388,10 +394,10 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
|
||||
// Third render has a changed reducer hook.
|
||||
utils.act(() => dispatch({type: 'invert'}));
|
||||
utils.act(() => realDispatch({type: 'invert'}));
|
||||
|
||||
// Fourth render has a changed state hook.
|
||||
utils.act(() => setState('def'));
|
||||
utils.act(() => realSetState('def'));
|
||||
|
||||
// Fifth render has a changed context value, but no changed hook.
|
||||
utils.act(() =>
|
||||
@@ -521,6 +527,238 @@ describe('ProfilingCache', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('should detect what hooks changed in a render with custom and composite hooks', () => {
|
||||
let snapshot = 0;
|
||||
let syncExternalStoreCallback;
|
||||
|
||||
function subscribe(callback) {
|
||||
syncExternalStoreCallback = callback;
|
||||
return () => {};
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Custom hook wrapping multiple primitive hooks
|
||||
function useCustomHook() {
|
||||
const [value, setValue] = React.useState('custom');
|
||||
React.useEffect(() => {}, [value]);
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
let setState = null;
|
||||
let startTransition = null;
|
||||
let actionStateDispatch = null;
|
||||
let setCustomValue = null;
|
||||
let setFinalState = null;
|
||||
|
||||
const Component = () => {
|
||||
// Hook 0: useState
|
||||
const [state, _setState] = React.useState('initial');
|
||||
setState = _setState;
|
||||
|
||||
// Hook 1: useSyncExternalStore (composite hook - internally uses multiple hooks)
|
||||
const storeValue = React.useSyncExternalStore(
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getSnapshot,
|
||||
);
|
||||
|
||||
// Hook 2: useTransition (composite hook - internally uses multiple hooks)
|
||||
const [isPending, _startTransition] = React.useTransition();
|
||||
startTransition = _startTransition;
|
||||
|
||||
// Hook 3: useActionState (composite hook - internally uses multiple hooks)
|
||||
const [actionState, _actionStateDispatch] = React.useActionState(
|
||||
(_prev, action) => action,
|
||||
'action-initial',
|
||||
);
|
||||
actionStateDispatch = _actionStateDispatch;
|
||||
|
||||
// Hook 4: useState inside custom hook (flattened)
|
||||
// Hook 5: useEffect inside custom hook (not stateful, won't show in changes)
|
||||
const [customValue, _setCustomValue] = useCustomHook();
|
||||
setCustomValue = _setCustomValue;
|
||||
|
||||
// Hook 6: direct useState at the end
|
||||
const [finalState, _setFinalState] = React.useState('final');
|
||||
setFinalState = _setFinalState;
|
||||
|
||||
return `${state}-${storeValue}-${isPending}-${actionState}-${customValue}-${finalState}`;
|
||||
};
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => render(<Component />));
|
||||
|
||||
// Save references before inspectHooks() overwrites them
|
||||
const realSetState = setState;
|
||||
const realStartTransition = startTransition;
|
||||
const realActionStateDispatch = actionStateDispatch;
|
||||
const realSetCustomValue = setCustomValue;
|
||||
const realSetFinalState = setFinalState;
|
||||
|
||||
// 2nd render: change useState (hook 0)
|
||||
utils.act(() => realSetState('changed'));
|
||||
|
||||
// 3rd render: change useSyncExternalStore (hook 1)
|
||||
utils.act(() => {
|
||||
snapshot = 1;
|
||||
syncExternalStoreCallback();
|
||||
});
|
||||
|
||||
// 4th render: trigger useTransition (hook 2)
|
||||
// Note: useTransition triggers two renders - one when isPending becomes true,
|
||||
// and another when isPending becomes false after the transition completes
|
||||
utils.act(() => {
|
||||
realStartTransition(() => {});
|
||||
});
|
||||
|
||||
// 6th render: change useActionState (hook 3)
|
||||
utils.act(() => realActionStateDispatch('action-changed'));
|
||||
|
||||
// 7th render: change custom hook's useState (hook 4)
|
||||
utils.act(() => realSetCustomValue('custom-changed'));
|
||||
|
||||
// 8th render: change final useState (hook 6)
|
||||
utils.act(() => realSetFinalState('final-changed'));
|
||||
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
const changeDescriptions = store.profilerStore
|
||||
.getDataForRoot(rootID)
|
||||
.commitData.map(commitData => commitData.changeDescriptions);
|
||||
expect(changeDescriptions).toHaveLength(8);
|
||||
|
||||
// 1st render: Initial mount
|
||||
expect(changeDescriptions[0]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": null,
|
||||
"didHooksChange": false,
|
||||
"isFirstMount": true,
|
||||
"props": null,
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 2nd render: Changed hook 0 (useState)
|
||||
expect(changeDescriptions[1]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
0,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 3rd render: Changed hook 1 (useSyncExternalStore)
|
||||
expect(changeDescriptions[2]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
1,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 4th render: Changed hook 2 (useTransition - isPending becomes true)
|
||||
expect(changeDescriptions[3]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
2,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 5th render: Changed hook 2 (useTransition - isPending becomes false)
|
||||
expect(changeDescriptions[4]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
2,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 6th render: Changed hook 3 (useActionState)
|
||||
expect(changeDescriptions[5]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
3,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 7th render: Changed hook 4 (useState inside useCustomHook)
|
||||
expect(changeDescriptions[6]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
4,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
// 8th render: Changed hook 6 (final useState)
|
||||
expect(changeDescriptions[7]).toMatchInlineSnapshot(`
|
||||
Map {
|
||||
2 => {
|
||||
"context": false,
|
||||
"didHooksChange": true,
|
||||
"hooks": [
|
||||
6,
|
||||
],
|
||||
"isFirstMount": false,
|
||||
"props": [],
|
||||
"state": null,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19.0
|
||||
it('should detect context changes or lack of changes with conditional use()', () => {
|
||||
const ContextA = React.createContext(0);
|
||||
@@ -553,6 +791,11 @@ describe('ProfilingCache', () => {
|
||||
),
|
||||
);
|
||||
|
||||
// Save reference to the real setState function before profiling starts.
|
||||
// inspectHooks() re-runs the component with a mock dispatcher,
|
||||
// which would overwrite setState with a mock function that does nothing.
|
||||
const realSetState = setState;
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
|
||||
// First render changes Context.
|
||||
@@ -567,7 +810,7 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
|
||||
// Second render has no changed Context, only changed state.
|
||||
utils.act(() => setState('def'));
|
||||
utils.act(() => realSetState('def'));
|
||||
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
|
||||
@@ -240,9 +240,6 @@ beforeEach(() => {
|
||||
setSavedComponentFilters(getDefaultComponentFilters());
|
||||
global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters();
|
||||
|
||||
// Also initialize inline warnings so that we can test them.
|
||||
global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
|
||||
|
||||
installHook(global, {
|
||||
appendComponentStack: true,
|
||||
breakOnConsoleErrors: false,
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import semver from 'semver';
|
||||
|
||||
import {getVersionedRenderImplementation} from './utils';
|
||||
import {ReactVersion} from '../../../../ReactVersions';
|
||||
|
||||
const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
|
||||
|
||||
describe('Store', () => {
|
||||
let React;
|
||||
@@ -3143,8 +3148,9 @@ describe('Store', () => {
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
|
||||
// @reactVersion >= 17.0
|
||||
it('should track suspended by in filtered fallback', async () => {
|
||||
// Can't suspend the root in React 17.
|
||||
// @reactVersion >= 18.0
|
||||
it('should track suspended-by in filtered fallback suspending the root', async () => {
|
||||
function IgnoreMe({promise}) {
|
||||
return readValue(promise);
|
||||
}
|
||||
@@ -3196,6 +3202,91 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 17.0
|
||||
it('should track suspended-by in filtered fallback', async () => {
|
||||
function IgnoreMe({promise}) {
|
||||
return readValue(promise);
|
||||
}
|
||||
|
||||
function Component({promise}) {
|
||||
if (promise) {
|
||||
return readValue(promise);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
await actAsync(
|
||||
async () =>
|
||||
(store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
|
||||
);
|
||||
|
||||
let resolveFallback;
|
||||
const fallbackPromise = new Promise(resolve => {
|
||||
resolveFallback = resolve;
|
||||
});
|
||||
let resolveContent;
|
||||
const contentPromise = new Promise(resolve => {
|
||||
resolveContent = resolve;
|
||||
});
|
||||
|
||||
await actAsync(() =>
|
||||
render(
|
||||
<React.Suspense
|
||||
fallback={<Component key="root-fallback" />}
|
||||
name="root">
|
||||
<React.Suspense
|
||||
name="main"
|
||||
fallback={<IgnoreMe promise={fallbackPromise} />}>
|
||||
<Component promise={contentPromise} />
|
||||
</React.Suspense>
|
||||
</React.Suspense>,
|
||||
),
|
||||
);
|
||||
|
||||
if (semver.lt(ReactVersionTestingAgainst, '18.0.0')) {
|
||||
// React 17 commits partial trees hidden which causes the "main"
|
||||
// Suspense boundary to be included.
|
||||
// React 18 and upwards excluded partial tree entirely.
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="root">
|
||||
<Component key="root-fallback">
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="root" rects={null}>
|
||||
<Suspense name="main" rects={null}>
|
||||
`);
|
||||
} else {
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="root">
|
||||
<Component key="root-fallback">
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="root" rects={null}>
|
||||
`);
|
||||
}
|
||||
|
||||
await actAsync(() => resolveFallback('loading'));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="root">
|
||||
<Suspense name="main">
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="root" rects={null}>
|
||||
<Suspense name="main" rects={null}>
|
||||
`);
|
||||
|
||||
await actAsync(() => resolveContent('content'));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Suspense name="root">
|
||||
▾ <Suspense name="main">
|
||||
<Component>
|
||||
[suspense-root] rects={null}
|
||||
<Suspense name="root" rects={null}>
|
||||
<Suspense name="main" rects={null}>
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 19
|
||||
it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
|
||||
const Activity = React.Activity || React.unstable_Activity;
|
||||
|
||||
@@ -938,11 +938,19 @@ export default class Agent extends EventEmitter<{
|
||||
}
|
||||
};
|
||||
|
||||
selectNode(target: HostInstance): void {
|
||||
const match = this.getIDForHostInstance(target);
|
||||
if (match !== null) {
|
||||
this._bridge.send('selectElement', match.id);
|
||||
}
|
||||
selectNode(target: HostInstance | null): void {
|
||||
const match = target !== null ? this.getIDForHostInstance(target) : null;
|
||||
this._bridge.send(
|
||||
'selectElement',
|
||||
match !== null
|
||||
? match.id
|
||||
: // If you click outside a React root in the Elements panel, we want to give
|
||||
// feedback that no selection is possible so we clear the selection.
|
||||
// Otherwise clicking outside a React root is indistinguishable from clicking
|
||||
// a different host node that leads to the same selected React element
|
||||
// due to Component filters
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
registerRendererInterface(
|
||||
@@ -988,10 +996,7 @@ export default class Agent extends EventEmitter<{
|
||||
|
||||
syncSelectionFromBuiltinElementsPanel: () => void = () => {
|
||||
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
this.selectNode(target);
|
||||
this.selectNode(target == null ? null : target);
|
||||
};
|
||||
|
||||
shutdown: () => void = () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
Wakeable,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
||||
import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
||||
|
||||
import {
|
||||
ComponentFilterDisplayName,
|
||||
@@ -127,7 +127,6 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
|
||||
import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';
|
||||
|
||||
import is from 'shared/objectIs';
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
|
||||
import {getIODescription} from 'shared/ReactIODescription';
|
||||
|
||||
@@ -1976,10 +1975,9 @@ export function attach(
|
||||
state: null,
|
||||
};
|
||||
} else {
|
||||
const indices = getChangedHooksIndices(
|
||||
prevFiber.memoizedState,
|
||||
nextFiber.memoizedState,
|
||||
);
|
||||
const prevHooks = inspectHooks(prevFiber);
|
||||
const nextHooks = inspectHooks(nextFiber);
|
||||
const indices = getChangedHooksIndices(prevHooks, nextHooks);
|
||||
const data: ChangeDescription = {
|
||||
context: getContextChanged(prevFiber, nextFiber),
|
||||
didHooksChange: indices !== null && indices.length > 0,
|
||||
@@ -2028,74 +2026,53 @@ export function attach(
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUseSyncExternalStoreHook(hookObject: any): boolean {
|
||||
const queue = hookObject.queue;
|
||||
if (!queue) {
|
||||
return false;
|
||||
}
|
||||
function didStatefulHookChange(prev: HooksNode, next: HooksNode): boolean {
|
||||
// Detect the shape of useState() / useReducer() / useTransition() / useSyncExternalStore() / useActionState()
|
||||
const isStatefulHook =
|
||||
prev.isStateEditable === true ||
|
||||
prev.name === 'SyncExternalStore' ||
|
||||
prev.name === 'Transition' ||
|
||||
prev.name === 'ActionState' ||
|
||||
prev.name === 'FormState';
|
||||
|
||||
const boundHasOwnProperty = hasOwnProperty.bind(queue);
|
||||
return (
|
||||
boundHasOwnProperty('value') &&
|
||||
boundHasOwnProperty('getSnapshot') &&
|
||||
typeof queue.getSnapshot === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function isHookThatCanScheduleUpdate(hookObject: any) {
|
||||
const queue = hookObject.queue;
|
||||
if (!queue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const boundHasOwnProperty = hasOwnProperty.bind(queue);
|
||||
|
||||
// Detect the shape of useState() / useReducer() / useTransition()
|
||||
// using the attributes that are unique to these hooks
|
||||
// but also stable (e.g. not tied to current Lanes implementation)
|
||||
// We don't check for dispatch property, because useTransition doesn't have it
|
||||
if (boundHasOwnProperty('pending')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isUseSyncExternalStoreHook(hookObject);
|
||||
}
|
||||
|
||||
function didStatefulHookChange(prev: any, next: any): boolean {
|
||||
const prevMemoizedState = prev.memoizedState;
|
||||
const nextMemoizedState = next.memoizedState;
|
||||
|
||||
if (isHookThatCanScheduleUpdate(prev)) {
|
||||
return prevMemoizedState !== nextMemoizedState;
|
||||
// Compare the values to see if they changed
|
||||
if (isStatefulHook) {
|
||||
return prev.value !== next.value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
|
||||
if (prev == null || next == null) {
|
||||
function getChangedHooksIndices(
|
||||
prevHooks: HooksTree | null,
|
||||
nextHooks: HooksTree | null,
|
||||
): null | Array<number> {
|
||||
if (prevHooks == null || nextHooks == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indices = [];
|
||||
const indices: Array<number> = [];
|
||||
let index = 0;
|
||||
|
||||
while (next !== null) {
|
||||
if (didStatefulHookChange(prev, next)) {
|
||||
indices.push(index);
|
||||
}
|
||||
function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
|
||||
for (let i = 0; i < prevTree.length; i++) {
|
||||
const prevHook = prevTree[i];
|
||||
const nextHook = nextTree[i];
|
||||
|
||||
// useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook
|
||||
if (isUseSyncExternalStoreHook(next)) {
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
}
|
||||
if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
|
||||
traverse(prevHook.subHooks, nextHook.subHooks);
|
||||
continue;
|
||||
}
|
||||
|
||||
next = next.next;
|
||||
prev = prev.next;
|
||||
index++;
|
||||
if (didStatefulHookChange(prevHook, nextHook)) {
|
||||
indices.push(index);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
traverse(prevHooks, nextHooks);
|
||||
return indices;
|
||||
}
|
||||
|
||||
|
||||
8
packages/react-devtools-shared/src/bridge.js
vendored
8
packages/react-devtools-shared/src/bridge.js
vendored
@@ -74,7 +74,7 @@ export const currentBridgeProtocol: BridgeProtocol =
|
||||
|
||||
type ElementAndRendererID = {id: number, rendererID: RendererID};
|
||||
|
||||
type Message = {
|
||||
export type Message = {
|
||||
event: string,
|
||||
payload: any,
|
||||
};
|
||||
@@ -191,7 +191,7 @@ type NativeStyleEditor_SetValueParams = {
|
||||
value: string,
|
||||
};
|
||||
|
||||
type SavedPreferencesParams = {
|
||||
export type SavedPreferencesParams = {
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
};
|
||||
|
||||
@@ -214,7 +214,7 @@ export type BackendEvents = {
|
||||
profilingStatus: [boolean],
|
||||
reloadAppForProfiling: [],
|
||||
saveToClipboard: [string],
|
||||
selectElement: [number],
|
||||
selectElement: [number | null],
|
||||
shutdown: [],
|
||||
stopInspectingHost: [boolean],
|
||||
scrollTo: [{left: number, top: number, right: number, bottom: number}],
|
||||
@@ -239,7 +239,7 @@ export type BackendEvents = {
|
||||
type StartProfilingParams = ProfilingSettings;
|
||||
type ReloadAndProfilingParams = ProfilingSettings;
|
||||
|
||||
type FrontendEvents = {
|
||||
export type FrontendEvents = {
|
||||
clearErrorsAndWarnings: [{rendererID: RendererID}],
|
||||
clearErrorsForElementID: [ElementAndRendererID],
|
||||
clearHostInstanceHighlight: [],
|
||||
|
||||
@@ -147,7 +147,7 @@ export default class Store extends EventEmitter<{
|
||||
enableSuspenseTab: [],
|
||||
error: [Error],
|
||||
hookSettings: [$ReadOnly<DevToolsHookSettings>],
|
||||
hostInstanceSelected: [Element['id']],
|
||||
hostInstanceSelected: [Element['id'] | null],
|
||||
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
|
||||
mutated: [
|
||||
[
|
||||
@@ -2381,8 +2381,15 @@ export default class Store extends EventEmitter<{
|
||||
this._bridge.send('getHookSettings'); // Warm up cached hook settings
|
||||
};
|
||||
|
||||
onHostInstanceSelected: (elementId: number) => void = elementId => {
|
||||
if (this._lastSelectedHostInstanceElementId === elementId) {
|
||||
onHostInstanceSelected: (elementId: number | null) => void = elementId => {
|
||||
if (
|
||||
this._lastSelectedHostInstanceElementId === elementId &&
|
||||
// Force clear selection e.g. when we inspect an element in the Components panel
|
||||
// and then switch to the browser's Elements panel.
|
||||
// We wouldn't want to stay on the inspected element if we're inspecting
|
||||
// an element not owned by React when switching to the browser's Elements panel.
|
||||
elementId !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
flex: 1 1 35%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ResizeBarWrapper {
|
||||
@@ -55,6 +56,7 @@
|
||||
|
||||
.InspectedElementWrapper {
|
||||
flex: 1 1 50%;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.ResizeBar {
|
||||
|
||||
@@ -159,7 +159,11 @@ function Components(_: {}) {
|
||||
<div className={styles.InspectedElementWrapper}>
|
||||
<NativeStyleContextController>
|
||||
<InspectedElementErrorBoundary>
|
||||
<InspectedElement />
|
||||
<InspectedElement
|
||||
fallbackEmpty={
|
||||
'No React element selected. Select an element in the tree to inspect.'
|
||||
}
|
||||
/>
|
||||
</InspectedElementErrorBoundary>
|
||||
</NativeStyleContextController>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@@ -69,7 +68,11 @@
|
||||
padding: 0.25rem;
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.NoInspectionFallback {
|
||||
padding: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.StrictModeNonCompliant {
|
||||
@@ -77,3 +80,11 @@
|
||||
padding: 0.25rem;
|
||||
color: var(--color-console-error-icon);
|
||||
}
|
||||
|
||||
.VRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
flex: 0 0 1px;
|
||||
margin: 0 0.5rem;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
@@ -34,13 +34,20 @@ import useEditorURL from '../useEditorURL';
|
||||
import styles from './InspectedElement.css';
|
||||
import Tooltip from './reach-ui/tooltip';
|
||||
|
||||
export type Props = {};
|
||||
export type Props = {
|
||||
actionButtons?: React.Node,
|
||||
/** fallback to show when no element is inspected */
|
||||
fallbackEmpty: React.Node,
|
||||
};
|
||||
|
||||
// TODO Make edits and deletes also use transition API!
|
||||
|
||||
const noSourcePromise = Promise.resolve(null);
|
||||
|
||||
export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
export default function InspectedElementWrapper({
|
||||
actionButtons,
|
||||
fallbackEmpty,
|
||||
}: Props): React.Node {
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
@@ -189,6 +196,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
return (
|
||||
<div className={styles.InspectedElement}>
|
||||
<div className={styles.TitleRow} />
|
||||
<div className={styles.NoInspectionFallback}>{fallbackEmpty}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -305,6 +313,13 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
/>
|
||||
)}
|
||||
|
||||
{actionButtons && (
|
||||
<>
|
||||
<div className={styles.VRule} />
|
||||
{actionButtons}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{inspectedElement === null && (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user