From 857f1ae1f893280609e21696dfd5bf74cfbd0564 Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Fri, 17 Jan 2025 14:02:53 +0800 Subject: [PATCH 1/5] feat: add leave handler to traverseFiber --- packages/bippy/src/core.ts | 66 ++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/bippy/src/core.ts b/packages/bippy/src/core.ts index 940daf60..04dde5b7 100644 --- a/packages/bippy/src/core.ts +++ b/packages/bippy/src/core.ts @@ -436,28 +436,76 @@ export const getNearestHostFibers = (fiber: Fiber): Fiber[] => { return hostFibers; }; -/** - * Traverses up or down a {@link Fiber}, return `true` to stop and select a node. - */ -export const traverseFiber = ( +export type FiberSelector = (node: Fiber) => boolean | undefined; + +export interface TraverseFiberOptions { + /** + * The handler to call when entering a fiber, return `true` to stop and select a node. + */ + enter?: FiberSelector; + /** + * The handler to call when exiting a fiber, return `true` to stop and select a node. + */ + leave?: FiberSelector; + /** + * Whether to traverse the fiber tree in ascending order. + */ + ascending?: boolean; +} + +export interface TraverseFiber { + ( + fiber: Fiber | null, + /** + * The handler to call when entering a fiber. + */ + selector: FiberSelector, + /** @deprecated In favor of `options.ascending`. */ + ascending?: boolean, + ): Fiber | null; + (fiber: Fiber | null, options: TraverseFiberOptions): Fiber | null; +} + +const traverseFiberImpl = ( fiber: Fiber | null, - // biome-ignore lint/suspicious/noConfusingVoidType: may or may not exist - selector: (node: Fiber) => boolean | void, - ascending = false, + options: TraverseFiberOptions, ): Fiber | null => { if (!fiber) return null; - if (selector(fiber) === true) return fiber; + const { enter, leave, ascending } = options; + + if (enter && enter(fiber) === true) return fiber; let child = ascending ? fiber.return : fiber.child; while (child) { - const match = traverseFiber(child, selector, ascending); + const match = traverseFiber(child, options); if (match) return match; child = ascending ? null : child.sibling; } + + if (leave && leave(fiber) === true) return fiber; + return null; }; +/** + * Traverses up or down a {@link Fiber}, return `true` to stop and select a node. + */ +export const traverseFiber: TraverseFiber = ( + fiber, + selectorOrOpts, + ascendingOrNever = false, +) => { + if (!fiber) return null; + if (typeof selectorOrOpts === 'function') { + const opts: TraverseFiberOptions = { enter: selectorOrOpts }; + if (typeof ascendingOrNever === 'boolean') + opts.ascending = ascendingOrNever; + return traverseFiberImpl(fiber, opts); + } + return traverseFiberImpl(fiber, selectorOrOpts); +}; + /** * Returns the timings of the {@link Fiber}. * From d722ac570483356e5e948685dd0cf7c5b5f450db Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Sun, 19 Jan 2025 17:03:08 +0800 Subject: [PATCH 2/5] test: add cases for `traverseFiber` --- packages/bippy/src/core.test.tsx | 49 ++++++++++++++++++++++++++++++++ setup-test.ts | 1 + tsconfig.json | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/bippy/src/core.test.tsx create mode 100644 setup-test.ts diff --git a/packages/bippy/src/core.test.tsx b/packages/bippy/src/core.test.tsx new file mode 100644 index 00000000..fbc9d1b1 --- /dev/null +++ b/packages/bippy/src/core.test.tsx @@ -0,0 +1,49 @@ +import type React from 'react'; +import { render, type RenderOptions } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { instrument, traverseFiber } from './core.js'; +import type { FiberRoot } from './types.js'; + +describe('traverseFiber', () => { + const onCommitFiberRoot = vi.fn(); + instrument({ onCommitFiberRoot }); + + const renderWithFiber = (ui: React.ReactNode, options?: RenderOptions) => { + const result = render(ui, options); + const fiber: FiberRoot = onCommitFiberRoot.mock.lastCall?.[1]; + return { ...result, fiber }; + }; + + const { fiber } = renderWithFiber( +
+
+
+
+
+
+
+
+
+
+
+
+
, + ); + + it('should traverse a fiber', () => { + const handler = vi.fn(); + traverseFiber(fiber.current, fiber => handler(fiber.key)); + const keys = handler.mock.calls.map(call => call[0]).slice(1); + const expected = ['root', 'a', 'a1', 'a2', 'b', 'c', 'd', 'd1', 'd11']; + expect(keys).toEqual(expected); + }); + + it('should traverse a fiber in reverse', () => { + const handler = vi.fn(); + const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); + expect(d11?.key).toBe('d11'); + traverseFiber(d11, fiber => handler(fiber.key), true); + const keys = handler.mock.calls.map(call => call[0]).slice(0, -1); + expect(keys).toEqual(['d11', 'd1', 'd', 'root']); + }); +}); diff --git a/setup-test.ts b/setup-test.ts new file mode 100644 index 00000000..3e19d4f7 --- /dev/null +++ b/setup-test.ts @@ -0,0 +1 @@ +import './src/rdt-hook'; diff --git a/tsconfig.json b/tsconfig.json index 4c009625..d903a3b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react", + "jsx": "react-jsx", "module": "NodeNext", "esModuleInterop": true, "strictNullChecks": true, From ab6b3c6c8cd964fc328145fe29534f76b1bb47c7 Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Mon, 20 Jan 2025 01:04:35 +0800 Subject: [PATCH 3/5] feat: implement stack-based `traverseFiber` --- packages/bippy/src/core.test.tsx | 109 ++++++++++++++++++++++++++++--- packages/bippy/src/core.ts | 76 +++++++++++++-------- 2 files changed, 147 insertions(+), 38 deletions(-) diff --git a/packages/bippy/src/core.test.tsx b/packages/bippy/src/core.test.tsx index fbc9d1b1..2b7e5a46 100644 --- a/packages/bippy/src/core.test.tsx +++ b/packages/bippy/src/core.test.tsx @@ -2,7 +2,7 @@ import type React from 'react'; import { render, type RenderOptions } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { instrument, traverseFiber } from './core.js'; -import type { FiberRoot } from './types.js'; +import type { Fiber, FiberRoot } from './types.js'; describe('traverseFiber', () => { const onCommitFiberRoot = vi.fn(); @@ -31,19 +31,108 @@ describe('traverseFiber', () => { ); it('should traverse a fiber', () => { - const handler = vi.fn(); - traverseFiber(fiber.current, fiber => handler(fiber.key)); - const keys = handler.mock.calls.map(call => call[0]).slice(1); - const expected = ['root', 'a', 'a1', 'a2', 'b', 'c', 'd', 'd1', 'd11']; - expect(keys).toEqual(expected); + const order: string[] = []; + traverseFiber(fiber.current, fiber => { + fiber.key && order.push(fiber.key); + }); + expect(order).toEqual([ + 'root', + 'a', + 'a1', + 'a2', + 'b', + 'c', + 'd', + 'd1', + 'd11', + ]); }); it('should traverse a fiber in reverse', () => { - const handler = vi.fn(); + const order: string[] = []; const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); expect(d11?.key).toBe('d11'); - traverseFiber(d11, fiber => handler(fiber.key), true); - const keys = handler.mock.calls.map(call => call[0]).slice(0, -1); - expect(keys).toEqual(['d11', 'd1', 'd', 'root']); + + traverseFiber( + d11, + fiber => { + fiber.key && order.push(fiber.key); + }, + true, + ); + expect(order).toEqual(['d11', 'd1', 'd', 'root']); + }); + + it('should traverse a fiber with entry and leave handlers', () => { + const enterOrder: string[] = []; + const leaveOrder: string[] = []; + traverseFiber(fiber.current, { + enter: fiber => { + fiber.key && enterOrder.push(fiber.key); + }, + leave: fiber => { + fiber.key && leaveOrder.push(fiber.key); + }, + }); + expect(enterOrder).toEqual([ + 'root', + 'a', + 'a1', + 'a2', + 'b', + 'c', + 'd', + 'd1', + 'd11', + ]); + expect(leaveOrder).toEqual([ + 'a1', + 'a2', + 'a', + 'b', + 'c', + 'd11', + 'd1', + 'd', + 'root', + ]); + }); + + it('should traverse a fiber with entry and leave handlers in reverse', () => { + const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); + expect(d11?.key).toBe('d11'); + + const enterOrder: string[] = []; + const leaveOrder: string[] = []; + traverseFiber(d11, { + ascending: true, + enter: fiber => { + fiber.key && enterOrder.push(fiber.key); + }, + leave: fiber => { + fiber.key && leaveOrder.push(fiber.key); + }, + }); + expect(enterOrder).toEqual(['d11', 'd1', 'd', 'root']); + expect(leaveOrder).toEqual(['root', 'd', 'd1', 'd11']); + }); + + it('should traverse a fiber and get stack', () => { + const stack: Fiber[] = []; + traverseFiber(fiber.current, { + enter: fiber => { + if (fiber.key === 'd11') { + const keys = stack.map(fiber => fiber.key).filter(Boolean); + expect(keys).toEqual(['root', 'd', 'd1']); + } + + stack.push(fiber); + }, + leave: fiber => { + const last = stack.pop(); + expect(last).toBe(fiber); + }, + }); + expect(stack).toEqual([]); }); }); diff --git a/packages/bippy/src/core.ts b/packages/bippy/src/core.ts index 04dde5b7..25baba82 100644 --- a/packages/bippy/src/core.ts +++ b/packages/bippy/src/core.ts @@ -436,7 +436,8 @@ export const getNearestHostFibers = (fiber: Fiber): Fiber[] => { return hostFibers; }; -export type FiberSelector = (node: Fiber) => boolean | undefined; +// biome-ignore lint/suspicious/noConfusingVoidType: +export type FiberSelector = (node: Fiber) => boolean | void; export interface TraverseFiberOptions { /** @@ -464,30 +465,13 @@ export interface TraverseFiber { ascending?: boolean, ): Fiber | null; (fiber: Fiber | null, options: TraverseFiberOptions): Fiber | null; + ( + fiber: Fiber | null, + selectorOrOpts: FiberSelector | TraverseFiberOptions, + ascendingOrNever?: boolean, + ): Fiber | null; } -const traverseFiberImpl = ( - fiber: Fiber | null, - options: TraverseFiberOptions, -): Fiber | null => { - if (!fiber) return null; - const { enter, leave, ascending } = options; - - if (enter && enter(fiber) === true) return fiber; - - let child = ascending ? fiber.return : fiber.child; - while (child) { - const match = traverseFiber(child, options); - if (match) return match; - - child = ascending ? null : child.sibling; - } - - if (leave && leave(fiber) === true) return fiber; - - return null; -}; - /** * Traverses up or down a {@link Fiber}, return `true` to stop and select a node. */ @@ -497,13 +481,49 @@ export const traverseFiber: TraverseFiber = ( ascendingOrNever = false, ) => { if (!fiber) return null; + let enter: FiberSelector | undefined; + let leave: FiberSelector | undefined; + let ascending = false; + if (typeof selectorOrOpts === 'function') { - const opts: TraverseFiberOptions = { enter: selectorOrOpts }; - if (typeof ascendingOrNever === 'boolean') - opts.ascending = ascendingOrNever; - return traverseFiberImpl(fiber, opts); + enter = selectorOrOpts; + if (typeof ascendingOrNever === 'boolean') ascending = ascendingOrNever; + } else { + enter = selectorOrOpts.enter; + leave = selectorOrOpts.leave; + ascending = selectorOrOpts.ascending ?? false; } - return traverseFiberImpl(fiber, selectorOrOpts); + + const stack: Fiber[] = [fiber]; + const visited = new Set(); + + while (stack.length > 0) { + const current = stack[stack.length - 1]; + + if (!visited.has(current)) { + visited.add(current); + + // Trigger enter handler only once per fiber. + if (enter && enter(current) === true) return current; + + // Keep going down the tree. We will back up later. + const next = ascending ? current.return : current.child; + if (next) { + stack.push(next); + continue; + } + } + + // Go back to the visited parent fiber and trigger leave handler. + stack.pop(); + + if (leave && leave(current) === true) return current; + + const sibling = ascending ? null : current.sibling; + if (sibling) stack.push(sibling); + } + + return null; }; /** From 1044194b7ba5f0c1613961dab37f45b386b48d6b Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Thu, 23 Jan 2025 00:21:35 +0800 Subject: [PATCH 4/5] test: move traverseFiber cases to test/core/fiber.test.tsx --- packages/bippy/src/core.test.tsx | 138 ------------------- packages/bippy/src/test/core/fiber.test.tsx | 144 +++++++++++++++++++- setup-test.ts | 1 - tsconfig.json | 2 +- 4 files changed, 140 insertions(+), 145 deletions(-) delete mode 100644 packages/bippy/src/core.test.tsx delete mode 100644 setup-test.ts diff --git a/packages/bippy/src/core.test.tsx b/packages/bippy/src/core.test.tsx deleted file mode 100644 index 2b7e5a46..00000000 --- a/packages/bippy/src/core.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import type React from 'react'; -import { render, type RenderOptions } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { instrument, traverseFiber } from './core.js'; -import type { Fiber, FiberRoot } from './types.js'; - -describe('traverseFiber', () => { - const onCommitFiberRoot = vi.fn(); - instrument({ onCommitFiberRoot }); - - const renderWithFiber = (ui: React.ReactNode, options?: RenderOptions) => { - const result = render(ui, options); - const fiber: FiberRoot = onCommitFiberRoot.mock.lastCall?.[1]; - return { ...result, fiber }; - }; - - const { fiber } = renderWithFiber( -
-
-
-
-
-
-
-
-
-
-
-
-
, - ); - - it('should traverse a fiber', () => { - const order: string[] = []; - traverseFiber(fiber.current, fiber => { - fiber.key && order.push(fiber.key); - }); - expect(order).toEqual([ - 'root', - 'a', - 'a1', - 'a2', - 'b', - 'c', - 'd', - 'd1', - 'd11', - ]); - }); - - it('should traverse a fiber in reverse', () => { - const order: string[] = []; - const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); - expect(d11?.key).toBe('d11'); - - traverseFiber( - d11, - fiber => { - fiber.key && order.push(fiber.key); - }, - true, - ); - expect(order).toEqual(['d11', 'd1', 'd', 'root']); - }); - - it('should traverse a fiber with entry and leave handlers', () => { - const enterOrder: string[] = []; - const leaveOrder: string[] = []; - traverseFiber(fiber.current, { - enter: fiber => { - fiber.key && enterOrder.push(fiber.key); - }, - leave: fiber => { - fiber.key && leaveOrder.push(fiber.key); - }, - }); - expect(enterOrder).toEqual([ - 'root', - 'a', - 'a1', - 'a2', - 'b', - 'c', - 'd', - 'd1', - 'd11', - ]); - expect(leaveOrder).toEqual([ - 'a1', - 'a2', - 'a', - 'b', - 'c', - 'd11', - 'd1', - 'd', - 'root', - ]); - }); - - it('should traverse a fiber with entry and leave handlers in reverse', () => { - const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); - expect(d11?.key).toBe('d11'); - - const enterOrder: string[] = []; - const leaveOrder: string[] = []; - traverseFiber(d11, { - ascending: true, - enter: fiber => { - fiber.key && enterOrder.push(fiber.key); - }, - leave: fiber => { - fiber.key && leaveOrder.push(fiber.key); - }, - }); - expect(enterOrder).toEqual(['d11', 'd1', 'd', 'root']); - expect(leaveOrder).toEqual(['root', 'd', 'd1', 'd11']); - }); - - it('should traverse a fiber and get stack', () => { - const stack: Fiber[] = []; - traverseFiber(fiber.current, { - enter: fiber => { - if (fiber.key === 'd11') { - const keys = stack.map(fiber => fiber.key).filter(Boolean); - expect(keys).toEqual(['root', 'd', 'd1']); - } - - stack.push(fiber); - }, - leave: fiber => { - const last = stack.pop(); - expect(last).toBe(fiber); - }, - }); - expect(stack).toEqual([]); - }); -}); diff --git a/packages/bippy/src/test/core/fiber.test.tsx b/packages/bippy/src/test/core/fiber.test.tsx index 7246e300..536b127c 100644 --- a/packages/bippy/src/test/core/fiber.test.tsx +++ b/packages/bippy/src/test/core/fiber.test.tsx @@ -1,5 +1,9 @@ -import { describe, expect, it } from 'vitest'; -import type { Fiber } from '../../types.js'; +// import bippy, then react +import { expect, it, describe, vi } from 'vitest'; +await import('../../index.js'); +// biome-ignore lint/correctness/noUnusedVariables: needed for JSX +const React = require('react'); +import type { Fiber, FiberRoot } from '../../types.js'; import { isValidFiber, isHostFiber, @@ -15,9 +19,7 @@ import { getFiberFromHostInstance, instrument, } from '../../index.js'; -// biome-ignore lint/correctness/noUnusedImports: needed for JSX -import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, type RenderOptions, screen } from '@testing-library/react'; import { BasicComponent, BasicComponentWithChildren, @@ -276,6 +278,138 @@ describe('traverseFiber', () => { ), ).toBe((maybeFiber as unknown as Fiber)?.child); }); + + + const onCommitFiberRoot = vi.fn(); + instrument({ onCommitFiberRoot }); + + const renderWithFiber = (ui: React.ReactNode, options?: RenderOptions) => { + const result = render(ui, options); + const fiber: FiberRoot = onCommitFiberRoot.mock.lastCall?.[1]; + return { ...result, fiber }; + }; + + const { fiber } = renderWithFiber( +
+
+
+
+
+
+
+
+
+
+
+
+
, + ); + + it('should traverse a fiber', () => { + const order: string[] = []; + traverseFiber(fiber.current, fiber => { + fiber.key && order.push(fiber.key); + }); + expect(order).toEqual([ + 'root', + 'a', + 'a1', + 'a2', + 'b', + 'c', + 'd', + 'd1', + 'd11', + ]); + }); + + it('should traverse a fiber in reverse', () => { + const order: string[] = []; + const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); + expect(d11?.key).toBe('d11'); + + traverseFiber( + d11, + fiber => { + fiber.key && order.push(fiber.key); + }, + true, + ); + expect(order).toEqual(['d11', 'd1', 'd', 'root']); + }); + + it('should traverse a fiber with entry and leave handlers', () => { + const enterOrder: string[] = []; + const leaveOrder: string[] = []; + traverseFiber(fiber.current, { + enter: fiber => { + fiber.key && enterOrder.push(fiber.key); + }, + leave: fiber => { + fiber.key && leaveOrder.push(fiber.key); + }, + }); + expect(enterOrder).toEqual([ + 'root', + 'a', + 'a1', + 'a2', + 'b', + 'c', + 'd', + 'd1', + 'd11', + ]); + expect(leaveOrder).toEqual([ + 'a1', + 'a2', + 'a', + 'b', + 'c', + 'd11', + 'd1', + 'd', + 'root', + ]); + }); + + it('should traverse a fiber with entry and leave handlers in reverse', () => { + const d11 = traverseFiber(fiber.current, fiber => fiber.key === 'd11'); + expect(d11?.key).toBe('d11'); + + const enterOrder: string[] = []; + const leaveOrder: string[] = []; + traverseFiber(d11, { + ascending: true, + enter: fiber => { + fiber.key && enterOrder.push(fiber.key); + }, + leave: fiber => { + fiber.key && leaveOrder.push(fiber.key); + }, + }); + expect(enterOrder).toEqual(['d11', 'd1', 'd', 'root']); + expect(leaveOrder).toEqual(['root', 'd', 'd1', 'd11']); + }); + + it('should traverse a fiber and get stack', () => { + const stack: Fiber[] = []; + traverseFiber(fiber.current, { + enter: fiber => { + if (fiber.key === 'd11') { + const keys = stack.map(fiber => fiber.key).filter(Boolean); + expect(keys).toEqual(['root', 'd', 'd1']); + } + + stack.push(fiber); + }, + leave: fiber => { + const last = stack.pop(); + expect(last).toBe(fiber); + }, + }); + expect(stack).toEqual([]); + }); }); describe('getFiberFromHostInstance', () => { diff --git a/setup-test.ts b/setup-test.ts deleted file mode 100644 index 3e19d4f7..00000000 --- a/setup-test.ts +++ /dev/null @@ -1 +0,0 @@ -import './src/rdt-hook'; diff --git a/tsconfig.json b/tsconfig.json index d903a3b6..4c009625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react-jsx", + "jsx": "react", "module": "NodeNext", "esModuleInterop": true, "strictNullChecks": true, From f041a5ba8d15c53f9d7528defb9c56b758342ada Mon Sep 17 00:00:00 2001 From: Asuka109 Date: Sat, 25 Jan 2025 22:40:34 +0800 Subject: [PATCH 5/5] test: add show case for analyzing router --- packages/bippy/package.json | 2 + packages/bippy/src/test/core/fiber.test.tsx | 152 +++++++++++++++++++- pnpm-lock.yaml | 37 +++++ tsconfig.json | 13 -- 4 files changed, 190 insertions(+), 14 deletions(-) delete mode 100644 tsconfig.json diff --git a/packages/bippy/package.json b/packages/bippy/package.json index 55da4e6c..12b799cb 100644 --- a/packages/bippy/package.json +++ b/packages/bippy/package.json @@ -109,6 +109,8 @@ "react-inspector": "^6.0.2", "react-reconciler": "^0.31.0", "react-refresh": "^0.16.0", + "react-router": "^6.26.0", + "react-router-dom": "^6.26.0", "terser": "^5.36.0", "tsup": "^8.2.4", "vitest": "^2.1.8" diff --git a/packages/bippy/src/test/core/fiber.test.tsx b/packages/bippy/src/test/core/fiber.test.tsx index 306d6562..ea299027 100644 --- a/packages/bippy/src/test/core/fiber.test.tsx +++ b/packages/bippy/src/test/core/fiber.test.tsx @@ -3,6 +3,7 @@ import { expect, it, describe, vi } from 'vitest'; await import('../../index.js'); // biome-ignore lint/correctness/noUnusedVariables: needed for JSX const React = require('react'); +import { createMemoryRouter, type UNSAFE_DataRouterContext, Outlet, RouterProvider, useLoaderData, type DataRouteMatch } from 'react-router-dom'; import type { Fiber, FiberRoot } from '../../types.js'; import { didFiberCommit, @@ -21,7 +22,7 @@ import { } from '../../index.js'; // FIXME(Alexis): Both React and @testing-library/react should be after index.js // but the linter/import sorter keeps moving them on top -import { render, type RenderOptions, screen } from '@testing-library/react'; +import { render, type RenderOptions, screen, waitFor } from '@testing-library/react'; import { BasicComponent, BasicComponentWithChildren, @@ -412,6 +413,147 @@ describe('traverseFiber', () => { }); expect(stack).toEqual([]); }); + + + it('should traverse fibers of Data Router', async () => { + const router = createMemoryRouter([ + { + path: "/", + loader: async () => { + return { message: "Root layout data" }; + }, + Component: function RootLayout() { + const { message } = useLoaderData() as { message: string }; + return ( +
+ +
+

{message}

+ +
+
+ ); + }, + children: [ + { + index: true, + loader: async () => { + return { message: "Home page data" }; + }, + Component: function HomePage() { + const { message } = useLoaderData() as { message: string }; + return

{message}

; + }, + }, + { + path: "user", + loader: async () => { + return { message: "User layout data" }; + }, + Component: function UserLayout() { + const { message } = useLoaderData() as { message: string }; + return ( +
+

{message}

+ +
+ ); + }, + children: [ + { + index: true, + loader: async () => { + return { users: ["user1", "user2", "user3"] }; + }, + Component:function UserListPage() { + const { users } = useLoaderData() as { users: string[] }; + return ( +
    + {users.map(user => ( +
  • {user}
  • + ))} +
+ ); + }, + }, + { + path: ":id", + loader: async ({ params }) => { + return { userId: params.id }; + }, + Component: function UserDetailPage() { + const { userId } = useLoaderData() as { userId: string }; + return

User ID: {userId}

; + }, + }, + ], + }, + ], + }, + ], { initialEntries: ['/', '/user', '/user/123'], future: { v7_relativeSplatPath: true } }); + + + const onCommitFiberRoot = vi.fn(); + instrument({ onCommitFiberRoot }); + + const { getByTestId } = render() + await waitFor(() => Promise.resolve()); + const root = onCommitFiberRoot.mock.lastCall?.[1]; + + const cb = vi.fn(); + + type UnwrapContext = T extends React.Context ? NonNullable : never; + type DataRouter = UnwrapContext['router']; + interface RouterData { + fiber: Fiber; + router: DataRouter; + matches: (DataRouteMatch & { element?: Element })[] + } + const returns: Fiber[] = []; + const routeFibers: Fiber[] = []; + let routerData: RouterData | null = null; + traverseFiber(root.current, { + enter(fiber) { + if (isRouterProviderFiber(fiber)) { + const router = fiber.memoizedProps.router as DataRouter; + const matches = router.state.matches.map(match => ({ ...match })); + routerData = { fiber, router, matches } + } + + if (isRenderedRouteFiber(fiber)) + routeFibers.unshift(fiber); + returns.unshift(fiber); + }, + leave(fiber) { + if (isRenderedRouteFiber(fiber)) + routeFibers.shift(); + returns.shift(); + + if (fiber === routerData?.fiber) { + cb(routerData); + routerData = null; + } + + if (routerData && fiber.stateNode instanceof Element) { + for (let i = 0; i < routeFibers.length; i++) { + routerData.matches[i].element = fiber.stateNode; + } + } + }, + }); + expect(cb).toHaveBeenCalledTimes(1); + + const data = cb.mock.lastCall?.[0] as RouterData; + expect(data).toEqual(expect.objectContaining({ + fiber: expect.any(Object), + router: expect.any(Object), + matches: expect.arrayContaining([ + expect.objectContaining({ pathname: '/', element: getByTestId('root-layout') }), + expect.objectContaining({ pathname: '/user', element: getByTestId('user-layout') }), + expect.objectContaining({ pathname: '/user/123', element: getByTestId('user-detail-page') }), + ]) + })) + }); }); describe('getFiberFromHostInstance', () => { @@ -422,3 +564,11 @@ describe('getFiberFromHostInstance', () => { expect(fiber?.type).toBe('div'); }); }); + +function isRouterProviderFiber(fiber: Fiber) { + return fiber.type === RouterProvider; +} + +function isRenderedRouteFiber(fiber: Fiber) { + return fiber.type?.name === 'RenderedRoute'; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8690e6ae..e41dc3bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,6 +76,12 @@ importers: react-refresh: specifier: ^0.16.0 version: 0.16.0 + react-router: + specifier: ^6.26.0 + version: 6.28.2(react@19.0.0) + react-router-dom: + specifier: ^6.26.0 + version: 6.28.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) terser: specifier: ^5.36.0 version: 5.36.0 @@ -1165,6 +1171,10 @@ packages: peerDependencies: react: '>=16.8' + '@remix-run/router@1.21.1': + resolution: {integrity: sha512-KeBYSwohb8g4/wCcnksvKTYlg69O62sQeLynn2YE+5z7JWEj95if27kclW9QqbrlsQ2DINI8fjbV3zyuKfwjKg==} + engines: {node: '>=14.0.0'} + '@rollup/rollup-android-arm-eabi@4.28.0': resolution: {integrity: sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==} cpu: [arm] @@ -2464,6 +2474,19 @@ packages: '@types/react': optional: true + react-router-dom@6.28.2: + resolution: {integrity: sha512-O81EWqNJWqvlN/a7eTudAdQm0TbI7hw+WIi7OwwMcTn5JMyZ0ibTFNGz+t+Lju0df4LcqowCegcrK22lB1q9Kw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.28.2: + resolution: {integrity: sha512-BgFY7+wEGVjHCiqaj2XiUBQ1kkzfg6UoKYwEe0wv+FF+HNPCxtS/MVPvLAPH++EsuCMReZl9RYVGqcHLk5ms3A==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -3963,6 +3986,8 @@ snapshots: dependencies: react: 19.0.0 + '@remix-run/router@1.21.1': {} + '@rollup/rollup-android-arm-eabi@4.28.0': optional: true @@ -5227,6 +5252,18 @@ snapshots: optionalDependencies: '@types/react': 19.0.4 + react-router-dom@6.28.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@remix-run/router': 1.21.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-router: 6.28.2(react@19.0.0) + + react-router@6.28.2(react@19.0.0): + dependencies: + '@remix-run/router': 1.21.1 + react: 19.0.0 + react-style-singleton@2.2.3(@types/react@19.0.4)(react@19.0.0): dependencies: get-nonce: 1.0.1 diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 4c009625..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "jsx": "react", - "module": "NodeNext", - "esModuleInterop": true, - "strictNullChecks": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "lib": ["esnext", "dom"] - }, - "include": ["packages"], - "exclude": ["**/node_modules/**", "**/dist/**"] -}