Compare commits

...

2 Commits

Author SHA1 Message Date
Rick Hanlon
611f677145 Add logs 2025-03-20 13:49:16 -04:00
Dan Abramov
7b90545420 wip 2025-03-19 04:42:31 +09:00
7 changed files with 540 additions and 2 deletions

View File

@@ -7,6 +7,8 @@
/* eslint-disable react-internal/no-production-logging */
import {CustomConsole} from '@jest/console';
const chalk = require('chalk');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
@@ -62,9 +64,40 @@ const patchConsoleMethod = (methodName, logged) => {
let logMethod;
export function patchConsoleMethods({includeLog} = {includeLog: false}) {
// Collapse console logs so they don't log file names.
// Argument is serialized when passed from jest-cli script through to setupTests.
const formatter = (type, message) => {
switch (type) {
case 'error':
return '\x1b[31m' + message + '\x1b[0m';
case 'warn':
return '\x1b[33m' + message + '\x1b[0m';
case 'log':
default:
return message;
}
};
global.console = new CustomConsole(process.stdout, process.stderr, formatter);
patchConsoleMethod('error', loggedErrors);
patchConsoleMethod('warn', loggedWarns);
const originalLog = console.log;
console.log = function (format, ...args) {
for (let i = 0; i < args.length; i++) {
if (typeof args[i] === 'object' && args[i] !== null && args[i].$$typeof) {
const render = args[i].render;
args[i] = [args[i].$$typeof, 'type', args[i].type];
if (render) {
args[i].push('render');
args[i].push(render);
}
}
}
originalLog(format, ...args);
};
// Only assert console.log isn't called in CI so you can debug tests in DEV.
// The matchers will still work in DEV, so you can assert locally.
if (includeLog) {

View File

@@ -327,6 +327,14 @@ export function isFunctionClassComponent(
// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
if (current.tag !== HostRoot) {
console.group('createWorkInProgress');
console.log(' (start)');
console.log(' tag ->', current.tag);
console.log(' type ->', current.type);
console.log(' elementType ->', current.elementType);
console.log('');
}
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
@@ -418,17 +426,34 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
}
if (__DEV__) {
if (current.tag !== HostRoot) {
console.log(' (dev)');
console.log(' current');
console.log(' tag ->', current?.tag);
console.log(' type ->', current?.type);
console.log(' elementType ->', current?.elementType);
console.log(' wip');
console.log(' tag ->', workInProgress.tag);
console.log(' type ->', workInProgress.type);
console.log(' elementType ->', workInProgress.elementType);
}
workInProgress._debugInfo = current._debugInfo;
workInProgress._debugNeedsRemount = current._debugNeedsRemount;
switch (workInProgress.tag) {
case FunctionComponent:
console.log('');
workInProgress.type = resolveFunctionForHotReloading(current.type);
// workInProgress.elementType = workInProgress.type;
break;
case SimpleMemoComponent:
console.log('');
workInProgress.type = resolveFunctionForHotReloading(current.type);
break;
case ClassComponent:
workInProgress.type = resolveClassForHotReloading(current.type);
break;
case ForwardRef:
console.log('');
workInProgress.type = resolveForwardRefForHotReloading(current.type);
break;
default:
@@ -436,6 +461,18 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
}
}
if (current.tag !== HostRoot) {
console.log(' (end)');
console.log(' current');
console.log(' tag ->', current?.tag);
console.log(' type ->', current?.type);
console.log(' elementType ->', current?.elementType);
console.log(' wip');
console.log(' tag ->', workInProgress.tag);
console.log(' type ->', workInProgress.type);
console.log(' elementType ->', workInProgress.elementType);
}
console.groupEnd('createWorkInProgress');
return workInProgress;
}
@@ -555,6 +592,11 @@ export function createFiberFromTypeAndProps(
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
if (typeof type !== 'string') {
console.group('createFiberFromTypeAndProps');
console.log(' type ->', type);
console.log('');
}
let fiberTag = FunctionComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
let resolvedType = type;
@@ -566,7 +608,13 @@ export function createFiberFromTypeAndProps(
}
} else {
if (__DEV__) {
const ogResolvedType = resolvedType;
resolvedType = resolveFunctionForHotReloading(resolvedType);
console.log('(early if) createFiberFromTypeAndProps resolved type');
console.log(' tag ->', fiberTag);
console.log(' before type ->', ogResolvedType);
console.log(' resolved type ->', resolvedType);
console.log('');
}
}
} else if (typeof type === 'string') {
@@ -734,6 +782,15 @@ export function createFiberFromTypeAndProps(
if (__DEV__) {
fiber._debugOwner = owner;
}
if (typeof type !== 'string') {
console.log('(end) createFiberFromTypeAndProps');
console.log(' tag ->', fiber.tag);
console.log(' og ->', type);
console.log(' resolved ->', fiber.type);
console.log(' elementType ->', fiber.elementType);
console.log('');
console.groupEnd('createFiberFromTypeAndProps');
}
return fiber;
}

View File

@@ -135,6 +135,7 @@ import {
resolveFunctionForHotReloading,
resolveForwardRefForHotReloading,
resolveClassForHotReloading,
isCompatibleFamilyForHotReloading,
} from './ReactFiberHotReloading';
import {
@@ -485,6 +486,7 @@ function updateMemoComponent(
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
console.group('updateMemoComponent');
if (current === null) {
const type = Component.type;
if (
@@ -506,6 +508,19 @@ function updateMemoComponent(
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
console.log(' current');
console.log(' needsRemount ->', current?._debugNeedsRemount);
console.log(' tag ->', current?.tag);
console.log(' type ->', current?.type);
console.log(' elementType ->', current?.elementType);
console.log(' wip');
console.log(' needsRemount ->', workInProgress?._debugNeedsRemount);
console.log(' tag ->', workInProgress?.tag);
console.log(' type ->', workInProgress?.type);
console.log(' elementType ->', workInProgress?.elementType);
console.log(' return updateSimpleMemoComponent');
console.groupEnd('updateMemoComponent');
return updateSimpleMemoComponent(
current,
workInProgress,
@@ -537,6 +552,13 @@ function updateMemoComponent(
workInProgress.mode,
renderLanes,
);
console.log(' child');
console.log(' needsRemount ->', child?._debugNeedsRemount);
console.log(' tag ->', child?.tag);
console.log(' type ->', child?.type);
console.log(' elementType ->', child?.elementType);
console.groupEnd('updateMemoComponent');
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
@@ -3819,6 +3841,22 @@ function beginWork(
renderLanes: Lanes,
): Fiber | null {
if (__DEV__) {
if (
workInProgress?.tag !== HostRoot &&
workInProgress.tag !== HostComponent
) {
console.group('beginWork');
console.log(' current');
console.log(' needsRemount ->', current?._debugNeedsRemount);
console.log(' tag ->', current?.tag);
console.log(' type ->', current?.type);
console.log(' elementType ->', current?.elementType);
console.log(' wip');
console.log(' needsRemount ->', workInProgress?._debugNeedsRemount);
console.log(' tag ->', workInProgress?.tag);
console.log(' type ->', workInProgress?.type);
console.log(' elementType ->', workInProgress?.elementType);
}
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
const copiedFiber = createFiberFromTypeAndProps(
@@ -3831,9 +3869,27 @@ function beginWork(
);
copiedFiber._debugStack = workInProgress._debugStack;
copiedFiber._debugTask = workInProgress._debugTask;
return remountFiber(current, workInProgress, copiedFiber);
console.log('copied fiber');
console.log(' original');
console.log(' tag -> ', workInProgress.tag);
console.log(' type -> ', workInProgress.type);
console.log(' elementType -> ', workInProgress.elementType);
console.log(' copy');
console.log(' tag -> ', copiedFiber.tag);
console.log(' type -> ', copiedFiber.type);
console.log(' elementType -> ', copiedFiber.elementType);
const remounted = remountFiber(current, workInProgress, copiedFiber);
console.log(' remounted');
console.log(' tag -> ', remounted.tag);
console.log(' type -> ', remounted.type);
console.log(' elementType -> ', remounted.elementType);
console.log('');
console.groupEnd('beginWork');
return remounted;
}
}
console.groupEnd('beginWork');
if (current !== null) {
const oldProps = current.memoizedProps;

View File

@@ -28,6 +28,8 @@ import {
ForwardRef,
MemoComponent,
SimpleMemoComponent,
HostRoot,
HostComponent,
} from './ReactWorkTags';
import {
REACT_FORWARD_REF_TYPE,
@@ -62,15 +64,40 @@ export const setRefreshHandler = (handler: RefreshHandler | null): void => {
};
export function resolveFunctionForHotReloading(type: any): any {
const location = new Error().stack
.split('\n')
.slice(2, 3)
.join('')
.replace(/(.*[\\/])/, '')
.replace(')', '');
if (__DEV__) {
if (resolveFamily === null) {
console.log(
' -> resolveFunctionForHotReloading',
location,
type,
'result -> disabled',
);
// Hot reloading is disabled.
return type;
}
const family = resolveFamily(type);
if (family === undefined) {
console.log(
' -> resolveFunctionForHotReloading',
location,
type,
'result -> not found',
);
return type;
}
console.log(
' -> resolveFunctionForHotReloading',
location,
family.current,
'result -> new family',
);
// Use the latest known implementation.
return family.current;
} else {
@@ -84,8 +111,20 @@ export function resolveClassForHotReloading(type: any): any {
}
export function resolveForwardRefForHotReloading(type: any): any {
const location = new Error().stack
.split('\n')
.slice(2, 3)
.join('')
.replace(/(.*[\\/])/, '')
.replace(')', '');
if (__DEV__) {
if (resolveFamily === null) {
console.log(
' -> resolveForwardRefForHotReloading',
location,
type,
'result -> disabled',
);
// Hot reloading is disabled.
return type;
}
@@ -109,11 +148,30 @@ export function resolveForwardRefForHotReloading(type: any): any {
if (type.displayName !== undefined) {
(syntheticType: any).displayName = type.displayName;
}
console.log(
' -> resolveForwardRefForHotReloading',
location,
syntheticType,
'result -> synthetic',
);
return syntheticType;
}
}
console.log(
' -> resolveForwardRefForHotReloading',
location,
type,
'result -> not found',
);
return type;
}
console.log(
' -> resolveForwardRefForHotReloading',
location,
type,
'result -> new family',
);
// Use the latest known implementation.
return family.current;
} else {
@@ -134,6 +192,11 @@ export function isCompatibleFamilyForHotReloading(
const prevType = fiber.elementType;
const nextType = element.type;
console.log('isCompatibleFamilyForHotReloading');
console.log(' type -> ', fiber.type);
console.log(' prevType -> ', prevType);
console.log(' nextType -> ', nextType);
// If we got here, we know types aren't === equal.
let needsCompareFamilies = false;
@@ -261,7 +324,7 @@ function scheduleFibersWithFamiliesRecursively(
staleFamilies: Set<Family>,
): void {
if (__DEV__) {
const {alternate, child, sibling, tag, type} = fiber;
const {alternate, child, sibling, tag, type, elementType} = fiber;
let candidateType = null;
switch (tag) {
@@ -283,6 +346,14 @@ function scheduleFibersWithFamiliesRecursively(
let needsRender = false;
let needsRemount = false;
const isTypeStale = staleFamilies.has(resolveFamily(type));
const isElementTypeStale = staleFamilies.has(resolveFamily(elementType));
const isCandidateTypeStale = staleFamilies.has(resolveFamily(elementType));
if (staleFamilies.has(resolveFamily(elementType))) {
needsRemount = true;
}
if (candidateType !== null) {
const family = resolveFamily(candidateType);
if (family !== undefined) {
@@ -307,6 +378,19 @@ function scheduleFibersWithFamiliesRecursively(
}
}
if (tag !== HostRoot && tag !== HostComponent) {
console.log('scheduleFibersWithFamiliesRecursively');
console.log(' type stale ->', isTypeStale);
console.log(' candidate stale ->', isCandidateTypeStale);
console.log(' element stale ->', isElementTypeStale);
console.log(' tag ->', tag);
console.log(' candidateType ->', candidateType);
console.log(' elementType ->', elementType);
console.log(' type ->', type);
console.log(' needs render ->', needsRender);
console.log(' needs remount ->', needsRemount);
}
if (needsRemount) {
fiber._debugNeedsRemount = true;
}

View File

@@ -3103,6 +3103,30 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
completedWork,
entangledRenderLanes,
);
if (
completedWork.tag !== HostRoot &&
completedWork.tag !== HostComponent
) {
console.log('');
console.group('complete fiber');
console.log(' current');
console.log(' tag ->', current?.tag);
console.log(' type ->', current?.type);
console.log(' elementType ->', current?.elementType);
console.log(' complete');
console.log(' tag ->', completedWork?.tag);
console.log(' type ->', completedWork?.type);
console.log(' elementType ->', completedWork?.elementType);
console.log(' next');
console.log(' tag ->', next?.tag);
console.log(' type ->', next?.type);
console.log(' elementType ->', next?.elementType);
console.log(' wip');
console.log(' tag ->', workInProgress?.tag);
console.log(' type ->', workInProgress?.type);
console.log(' elementType ->', workInProgress?.elementType);
console.groupEnd('complete fiber');
}
} else {
next = completeWork(current, completedWork, entangledRenderLanes);
}

View File

@@ -146,6 +146,12 @@ function canPreserveStateBetween(prevType: any, nextType: any) {
if (isReactClass(prevType) || isReactClass(nextType)) {
return false;
}
if (
typeof prevType !== typeof nextType ||
getProperty(prevType, '$$typeof') !== getProperty(nextType, '$$typeof')
) {
return false;
}
if (haveEqualSignatures(prevType, nextType)) {
return true;
}
@@ -198,6 +204,7 @@ export function performReactRefresh(): RefreshUpdate | null {
isPerformingRefresh = true;
try {
console.group('React Refresh');
const staleFamilies = new Set<Family>();
const updatedFamilies = new Set<Family>();
@@ -213,8 +220,18 @@ export function performReactRefresh(): RefreshUpdate | null {
// Determine whether this should be a re-render or a re-mount.
if (canPreserveStateBetween(prevType, nextType)) {
console.log('REFRESH: updated');
console.log(' prev ->', prevType);
console.log(' next ->', nextType);
console.log('');
updatedFamilies.add(family);
} else {
console.log('REFRESH: stale');
console.log(' prev ->', prevType);
console.log(' next ->', nextType);
console.log('');
staleFamilies.add(family);
}
});
@@ -295,6 +312,7 @@ export function performReactRefresh(): RefreshUpdate | null {
return update;
} finally {
isPerformingRefresh = false;
console.groupEnd('React Refresh');
}
}

View File

@@ -699,6 +699,272 @@ describe('ReactFresh', () => {
}
});
fit('can remount when change function to memo', async () => {
if (__DEV__) {
console.group('\n\n=== initial render\n\n');
await act(async () => {
await render(() => {
function Test() {
return <p>hi test</p>;
}
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check the initial render
const el = container.firstChild;
expect(el.textContent).toBe('hi test');
console.groupEnd('\n\n=== initial render\n\n');
console.group('\n\n=== patch to memo\n\n');
// Patch to change function to memo
await act(async () => {
await patch(() => {
function Test2() {
return <p>hi memo</p>;
}
const Test = React.memo(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check remount
expect(container.firstChild !== el).toBe(true);
const nextEl = container.firstChild;
expect(nextEl.textContent).toBe('hi memo');
console.groupEnd('\n\n=== patch to memo\n\n');
console.group('\n\n=== patch to original\n\n');
// Patch back to original function
await act(async () => {
await patch(() => {
function Test() {
return <p>hi test</p>;
}
$RefreshReg$(Test, 'Test');
return Test;
});
});
console.groupEnd('\n\n=== patch to original\n\n');
// Check final remount
expect(container.firstChild !== nextEl).toBe(true);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('hi test');
}
});
fit('can remount when change memo to forwardRef', async () => {
if (__DEV__) {
console.group('\n\n=== initial render as memo) \n\n');
await act(async () => {
await render(() => {
function Test2() {
return <p>hi memo</p>;
}
const Test = React.memo(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check the initial render
const el = container.firstChild;
expect(el.textContent).toBe('hi memo');
console.groupEnd('\n\n=== initial render as memo) \n\n');
console.group('\n\n=== patch to fowardRef\n\n');
// Patch to change memo to forwardRef
await act(async () => {
await patch(() => {
function Test2() {
return <p>hi forwardRef</p>;
}
const Test = React.forwardRef(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check remount
expect(container.firstChild !== el).toBe(true);
const nextEl = container.firstChild;
expect(nextEl.textContent).toBe('hi forwardRef');
console.groupEnd('\n\n=== patch to fowardRef\n\n');
console.group('\n\n=== patch back to memo\n\n');
// Patch back to memo
await act(async () => {
await patch(() => {
function Test2() {
return <p>hi memo</p>;
}
const Test = React.memo(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
console.groupEnd('\n\n=== patch back to memo\n\n');
// Check final remount
expect(container.firstChild !== nextEl).toBe(true);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('hi memo');
}
});
fit('can remount when change function to forwardRef', async () => {
if (__DEV__) {
console.group('\n\n=== initial render\n\n');
await act(async () => {
await render(() => {
function Test() {
return <p>hi test</p>;
}
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check the initial render
const el = container.firstChild;
expect(el.textContent).toBe('hi test');
console.groupEnd('\n\n=== initial render\n\n');
console.group('\n\n=== patch to forwardRef \n\n');
// Patch to change function to forwardRef
await act(async () => {
await patch(() => {
function Test2() {
return <p>hi forwardRef</p>;
}
const Test = React.forwardRef(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
// Check remount
expect(container.firstChild !== el).toBe(true);
const nextEl = container.firstChild;
expect(nextEl.textContent).toBe('hi forwardRef');
console.groupEnd('\n\n=== patch to forwardRef \n\n');
console.group('\n\n=== patch to original \n\n');
// Patch back to a new function
await act(async () => {
await patch(() => {
function Test() {
return <p>hi test1</p>;
}
$RefreshReg$(Test, 'Test');
return Test;
});
});
console.groupEnd('\n\n=== patch to original \n\n');
// Check final remount
expect(container.firstChild !== nextEl).toBe(true);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('hi test1');
}
});
fit('resets state when switching between different component types', async () => {
if (__DEV__) {
console.group('\n\n=== initial render \n\n');
await act(async () => {
await render(() => {
function Test() {
const [count, setCount] = React.useState(0);
return (
<div onClick={() => setCount(c => c + 1)}>count: {count}</div>
);
}
$RefreshReg$(Test, 'Test');
return Test;
});
});
expect(container.firstChild.textContent).toBe('count: 0');
await act(async () => {
container.firstChild.click();
});
expect(container.firstChild.textContent).toBe('count: 1');
console.groupEnd('\n\n=== initial render \n\n');
console.group('\n\n=== patch to memo \n\n');
await act(async () => {
await patch(() => {
function Test2() {
const [count, setCount] = React.useState(0);
return (
<div onClick={() => setCount(c => c + 1)}>count: {count}</div>
);
}
const Test = React.memo(Test2);
$RefreshReg$(Test2, 'Test2');
$RefreshReg$(Test, 'Test');
return Test;
});
});
console.groupEnd('\n\n=== patch to memo \n\n');
console.group('\n\n=== click with memo \n\n');
expect(container.firstChild.textContent).toBe('count: 0');
await act(async () => {
container.firstChild.click();
});
expect(container.firstChild.textContent).toBe('count: 1');
console.groupEnd('\n\n=== click with memo \n\n');
console.group('\n\n=== patch to forwardRed \n\n');
await act(async () => {
await patch(() => {
const Test = React.forwardRef((props, ref) => {
const [count, setCount] = React.useState(0);
const handleClick = () => setCount(c => c + 1);
// Ensure ref is extensible
const divRef = React.useRef(null);
React.useEffect(() => {
if (ref) {
if (typeof ref === 'function') {
ref(divRef.current);
} else if (Object.isExtensible(ref)) {
ref.current = divRef.current;
}
}
}, [ref]);
return (
<div ref={divRef} onClick={handleClick}>
count: {count}
</div>
);
});
$RefreshReg$(Test, 'Test');
return Test;
});
});
console.groupEnd('\n\n=== patch to forwardRed \n\n');
console.group('\n\n=== click with forwardRed \n\n');
expect(container.firstChild.textContent).toBe('count: 0');
await act(async () => {
container.firstChild.click();
});
console.groupEnd('\n\n=== click with forwardRed \n\n');
expect(container.firstChild.textContent).toBe('count: 1');
}
});
it('can update simple memo function in isolation', async () => {
if (__DEV__) {
await render(() => {