diff --git a/.changeset/mean-facts-swim.md b/.changeset/mean-facts-swim.md new file mode 100644 index 00000000..734e5f6a --- /dev/null +++ b/.changeset/mean-facts-swim.md @@ -0,0 +1,25 @@ +--- +"react-router-devtools": major +--- + +Migration to TanStack Devtools is here! 🎉 + +You can now leverage the powerful features of TanStack Devtools within React Router Devtools, enhancing your debugging and development experience. + +You can easily create your own devtool plugins, inspect application state, and trace network requests with improved visibility. + +### Key Changes: +- **TanStack Integration**: Seamless integration with TanStack Devtools for advanced debugging capabilities +- **Enhanced UI**: New panels and tabs for better state and network inspection +- **Improved Performance**: Optimized for faster load times and reduced overhead +- **Middleware Support**: Ability to log middleware events and actions for deeper insights and also see them on the network tab +- **Extended Configuration**: New configuration options to customize TanStack Devtools behavior alongside React Router + +### Migration Steps: +1. Update your configuration to include TanStack-specific options. +2. Review and adjust any custom plugins to ensure compatibility with the new TanStack integration. +3. Test your application to verify that all devtools features are functioning as expected. + +### Features that have been removed: +- The route creation via devtools UI has been removed. +- Server info on the active page tab has been removed. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..49e48832 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,28 @@ +--- +applyTo: '**' +--- +Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes. + +Whenever you want to build the packages to test if they work you should run `pnpm run build` from the root of the repository. + +If you want to run tests you can run `pnpm run test` from the root of the repository. + +If you want to check if the examples work you need to go to `test-apps/` and run `pnpm run dev`. + +When writing code, please follow these guidelines: +- Use TypeScript for all new code. +- Ensure all new code is covered by tests. +- Do not use `any` type; prefer specific types or generics. +- Follow existing code style and conventions. + +If you get an error "address already in use :::42069 you should kill the process using that port. + +If we add a new functionality add a section about it in the `docs/content` folder explaining how to use it and update the `README.md` file to mention it. + +Write tests for any new functionality. + +When defining new types, first check if the types exist somewhere and re-use them, do not create new types that are similar to existing ones. + +When modifying existing functionality, ensure backward compatibility unless there's a strong reason to introduce breaking changes. If breaking changes are necessary, document them clearly in the relevant documentation files. + +If `pnpm run test` fails because of check, you can run `pnpm run check:fix` to fix the issues automatically. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3a910a1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Forge 42 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1b98596d..b100318f 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,6 @@ If you're trying to spin it up on CF, try adding this to your `optimizeDeps` in optimizeDeps: { include: [ // other optimized deps - "beautify", - "react-diff-viewer-continued", - "classnames", - "@bkrem/react-transition-group", ], }, ``` diff --git a/docs/app/routes/index.tsx b/docs/app/routes/index.tsx index 76b20017..4903906c 100644 --- a/docs/app/routes/index.tsx +++ b/docs/app/routes/index.tsx @@ -7,8 +7,7 @@ import { Meteors } from "~/components/ui/Meteors" import { InfiniteMovingCards } from "~/components/ui/infinite-cards" import { Navbar } from "~/components/ui/navbar-menu" import { TypewriterEffect } from "~/components/ui/typewritter" -import { Route } from "./+types" -import { buildDocPathFromSlug } from "~/utils/path-builders" +import type { Route } from "./+types" import { generateMetaFields } from "~/utils/seo" import { getDomain } from "~/utils/get-domain" @@ -97,7 +96,7 @@ export function OpenSourceReveal() { } className="h-[40rem]" > - Click Shift + Right Click to directly go to element source in + Click Shift + Ctrl + Left Click to directly go to element source in VS Code 🔥 diff --git a/docs/content/01-started/01-installation.mdx b/docs/content/01-started/01-installation.mdx index 97cc2fcd..09ddd3fe 100644 --- a/docs/content/01-started/01-installation.mdx +++ b/docs/content/01-started/01-installation.mdx @@ -14,7 +14,7 @@ npm install react-router-devtools -D This will install it as a dev dependency in your project. -## Enabling the tools +## Enabling the tools (framework mode) After you have installed the tools, you need to go to your `vite.config.ts` file which will probably look something like this: @@ -47,6 +47,36 @@ export default defineConfig({ Make sure your plugin is BEFORE the react router one! + +## Enabling the tools (data/declarative mode) + +If you are using React Router in data/declarative mode, you can still use the devtools +by first installing @tanstack/react-devtools and then adding the devtools somewhere under +the router provider in your app. + +```tsx +import { TanStackDevtools } from '@tanstack/react-devtools'; +import { EmbeddedDevTools } from 'react-router-devtools'; + +export function App() { + return ( + <> + + + + }] /> + + + + ); +} +``` + +react-router-devtools uses @tanstack/devtools as the base for the UI, you can refer to their +[documentation](https://tanstack.com/devtools/latest/docs/overview) for more information on how to use the devtools interface. + ### CloudFlare If you're trying to spin it up on CF, try adding this to your `optimizeDeps` in your `vite.config.js` file: @@ -54,10 +84,6 @@ If you're trying to spin it up on CF, try adding this to your `optimizeDeps` in optimizeDeps: { include: [ // other optimized deps - "beautify", - "react-diff-viewer-continued", - "classnames", - "@bkrem/react-transition-group", ], }, ``` diff --git a/docs/content/02-features/02-active-page-tab.mdx b/docs/content/02-features/01-active-page-tab.mdx similarity index 66% rename from docs/content/02-features/02-active-page-tab.mdx rename to docs/content/02-features/01-active-page-tab.mdx index 4fd929c7..93f3e773 100644 --- a/docs/content/02-features/02-active-page-tab.mdx +++ b/docs/content/02-features/01-active-page-tab.mdx @@ -16,9 +16,6 @@ gradient background that can be set in the settings. This feature is only available in the development mode because it used react dev tools to find the `` component. - If you want to try it open up the dev tools right now nad hover over `/docs/main` in the panel. - - You can also change the gradient background color in the settings. ## Loader list @@ -33,16 +30,6 @@ the loader type and the loader file. - `green` - represents a normal route file, whether index or not -### Open in VS code - -Each segment has an **open in VS code** button that opens the loader file in VS code. -This is useful for quick navigation to the loader file. - - -This only works if you have the `code` command installed in your terminal. If you don't have it installed you can - install it by following the instructions [here](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line). - - ### Loader data Each segment has a loader data **JSON** object that contains all the information returned by the loader of that segment. @@ -66,19 +53,6 @@ This will contain all the **wildcard** params currently available on this route. If you open up the dev tools, you will be able to see that `tag` and `slug` are both in the route params. -### Server info - -The server info section contains all the server information for the current segment, including: -- `loaderTriggerCount` - the number of times the loader has been triggered (updates in real-time) -- `actionTriggerCount` - the number of times the action has been triggered (updates in real-time) -- `lowestExecutionTime` - the lowest execution time of the loader (updates in real-time) -- `highestExecutionTime` - the highest execution time of the loader (updates in real-time) -- `averageExecutionTime` - the average execution time of the loader (updates in real-time) -- `lastLoaderInfo` - the last loader info object (updates in real-time), includes execution time, request headers and response headers. -- `lastActionInfo` - the last action info object (updates in real-time), includes execution time, request headers and response headers. -- `loaderCalls` - an array of loaderInfo objects ordered from most recent to least recent (updates in real-time) -- `actionCalls` - an array of actionInfo objects ordered from most recent to least recent (updates in real-time) - ### handles The handles section contains all the handles for the current segment. @@ -98,7 +72,7 @@ The timeline section on the right contains useful information on navigation and Every time there is a navigation or submission event, a new entry will be added to the timeline on the top. -It is limited to 50 entries and will remove the oldest entry when the limit is reached. +It is limited to 30 entries and will remove the oldest entry when the limit is reached. The timeline will contain the following information for each event: - `type` - the type of event (navigation or submission, fetcher or normal) diff --git a/docs/content/02-features/01-shortcuts.mdx b/docs/content/02-features/01-shortcuts.mdx deleted file mode 100644 index 3a5c3427..00000000 --- a/docs/content/02-features/01-shortcuts.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: "Keyboard Shortcuts" -summary: "Keyboard shortcuts in React Router Devtools let you quickly open source code, toggle the DevTools, and customize hotkeys via settings for faster navigation and debugging." -description: "Detailed overview of all keyboard shortcuts in React Router Devtools" ---- - -## Go To Source - -**Shift + Right Click** - -When you are in the browser and you want to go to the source code of a component, you can right click on the component -while holding down shift. This will open the source code of the component in your code editor. - -## Opening/closing the DevTools - -**Shift + A** - -When you are in the browser and you want to open the React Router Devtools, you can press `Shift + A`. -This will open the DevTools, if you're already in the DevTools, it will close it. - -While in the DevTools, you can also use `Esc` to close them. - -From version 4.2.0 is fully configurable and you can change the shortcut in the settings. - -We use [react-hotkeys-hook](https://www.npmjs.com/package/react-hotkeys-hook) to handle the keyboard shortcuts under the hood. -You can adapt to their API to add your own shortcuts. - -Check out the settings tab for details \ No newline at end of file diff --git a/docs/content/02-features/03-routes-tab.mdx b/docs/content/02-features/02-routes-tab.mdx similarity index 100% rename from docs/content/02-features/03-routes-tab.mdx rename to docs/content/02-features/02-routes-tab.mdx diff --git a/docs/content/02-features/07-devtools.mdx b/docs/content/02-features/03-devtools.mdx similarity index 73% rename from docs/content/02-features/07-devtools.mdx rename to docs/content/02-features/03-devtools.mdx index 1b7faa16..53567e18 100644 --- a/docs/content/02-features/07-devtools.mdx +++ b/docs/content/02-features/03-devtools.mdx @@ -4,6 +4,17 @@ summary: "The Devtools context provides tracing utilities for loaders and action description: "Using the devtools context to trace events and send them to the network tab" --- +## TanStack DevTools Integration + +React Router Devtools v6+ integrates with [TanStack DevTools](https://tanstack.com/devtools), providing enhanced debugging capabilities alongside React Router specific features. The devtools now include: + +- React Router specific tabs (Active Page, Routes, Network, Timeline, Settings) +- TanStack DevTools panels for advanced state inspection +- Unified debugging experience with seamless integration + +You can configure TanStack-specific behavior through the [general configuration](/configuration/general#tanstack-devtools-integration). + +--- ## Devtools extended context @@ -19,9 +30,9 @@ export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { const tracing = devTools?.tracing; // tracing is a set of utilities to be used in your data fetching functions to trace events // in network tab of react-router-devtools - const startTime = tracing.start("my-event") + const end = tracing.start("my-event") // do something here, eg DB call - tracing.end("my-event", startTime!) + end() return "data" } ``` @@ -33,9 +44,9 @@ export const action = async ({ request, devTools }: ActionFunctionArgs) => { const tracing = devTools?.tracing; // tracing is a set of utilities to be used in your data fetching functions to trace events // in network tab of react-router-devtools - const startTime = tracing?.start("my-event") + const end = tracing?.start("my-event") // do something - tracing?.end("my-event", startTime!) + end() return "data" } ``` @@ -47,9 +58,9 @@ export const clientLoader = async ({ request, devTools }: ClientLoaderFunctionAr const tracing = devTools?.tracing; // tracing is a set of utilities to be used in your data fetching functions to trace events // in network tab of react-router-devtools - const startTime = tracing?.start("my-event") + const end = tracing?.start("my-event") // do something - tracing?.end("my-event", startTime!) + end() return "data" } ``` @@ -59,9 +70,9 @@ export const clientAction = async ({ request, devTools }: ClientActionFunctionAr const tracing = devTools?.tracing; // tracing is a set of utilities to be used in your data fetching functions to trace events // in network tab of react-router-devtools - const startTime = tracing?.start("my-event") + const end = tracing?.start("my-event") // do something - tracing?.end("my-event", startTime!) + end() return "data" } ``` @@ -103,10 +114,9 @@ const loader = async ({ request, devTools }: LoaderFunctionArgs) => { The tracing object contains all the utilities related to network tab tracing feature of react-router-devtools. -There are three functions you can use: +There are two functions you can use: - trace - start -- end @@ -143,18 +153,18 @@ This is used together with `end` to trace the time of the event. export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { const tracing = devTools?.tracing; // this will be traced in the network tab of react-router-devtools - const startTime = tracing?.start("my-event") + const end = tracing?.start("my-event") // do something here, eg DB call // End the trace - tracing?.end("my-event", startTime!) + end() return "data" } ``` - This function relies on you using the `end` with the same name as the start event, otherwise -you will end up having a never ending loading bar in the network tab! + This function relies on you using the `end`returned from it, otherwise the event + will never end in your devtools @@ -164,32 +174,4 @@ you will end up having a never ending loading bar in the network tab! #### Returns -The start time of the event - -### end - -The `end` function is a function that will end a trace for the name provided to it and return the end time. - -```ts -export const loader = async ({ request, devTools }: LoaderFunctionArgs) => { - const tracing = devTools?.tracing; - // this will be traced in the network tab of react-router-devtools - const startTime = tracing?.start("get user") - // do something here, eg DB call - const user = await getUser(); - // End the trace - tracing?.end("get user", startTime!, { user }) - return "data" - -} -``` - -#### Parameters - -- `name` - The name of the event -- `startTime` - The start time of the sendEvent -- `data` - The data to be sent with the event - -#### Returns - -The data provided in the last parameter +The end trace function diff --git a/docs/content/02-features/04-errors-tab.mdx b/docs/content/02-features/04-errors-tab.mdx deleted file mode 100644 index d3d87b89..00000000 --- a/docs/content/02-features/04-errors-tab.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Errors Tab" -summary: "The Errors tab helps debug invalid HTML nesting and hydration mismatches by showing errors, file locations, and diffs between server and client HTML." -description: "Detailed overview of all features on the Errors Tab." ---- - -The errors tab is a powerful tool for debugging issues with your react code, namely invalid HTML. -It helps you detect potential HTML issues in your code, such as invalid HTML nesting or hydration issues. - -## Invalid HTML - -If you have invalidely nested HTML (eg. a `div` inside a `p`), you will see an error in the errors tab. -These kind of nesting issues can cause unexpected behavior in your application, namely hydration issues. -The browser does a lot of work to make sure that the HTML you send to the client is valid so it can -sometimes move the order of elements around to make sure it's valid. This can cause unexpected hydration -issues in your application. - -Whenever there is a case of this found in your html the errors tab will show you the error and the file -where the error is found. If the error is found in a file that is a part of your project you can click on the -file name to open the file in your editor and change the issue right away. - -## Hydration Mismatch - -Hydration mismatch is a common issue in React applications. It occurs when the server-rendered HTML does not match the -HTML generated by the client. This can cause unexpected behavior in your application, -such as the loss of user input or the loss of scroll position. In React Router it can also cause FOUC (Flash of Unstyled Content). - -To avoid hydration mismatch, you should make sure that the HTML generated by the server matches the HTML generated by -the client. - -These kind of issues are very hard to track down because they can be caused by a lot of different things. - -If a hydration mismatch happens the errors tab will show you the **diff** between the server and client HTML, allowing -you to analyze the differences and fix the issue. - - -Hydration mismatches happen on document requests (hard refresh or initial load in React Router). So if you don't see it at first -try refreshing your page. - \ No newline at end of file diff --git a/docs/content/02-features/08-network-tab.mdx b/docs/content/02-features/04-network-tab.mdx similarity index 78% rename from docs/content/02-features/08-network-tab.mdx rename to docs/content/02-features/04-network-tab.mdx index 64f65583..84946bf0 100644 --- a/docs/content/02-features/08-network-tab.mdx +++ b/docs/content/02-features/04-network-tab.mdx @@ -13,11 +13,13 @@ To shuffle through the requests, press the `←` and `→` keys. ## Request types -There are four types of requests in react-router-devtools: +There are six types of data handling exports in react-router-devtools: - **client-loader** - A client-loader is a request that is initiated by the client and is used to load data for a route. - **client-action** - A client-action is a request that is initiated by the client and is used to submit data to a route. - **loader** - A loader is a request that is initiated by the server and is used to load data for a route. - **action** - An action is a request that is initiated by the server and is used to submit data to a route. +- **middleware** - A middleware is run before and after all your loaders on the server +- **client-middleware** - A client-middleware is run before and after all your client-loaders on the client Each of these is colored differently for you to be able to quickly identify them. @@ -25,9 +27,18 @@ There are four types of requests in react-router-devtools: - client-loader - blue - action - purple - client-action - yellow +- middleware - orange +- client-middleware - pink - aborted requests - red + + +## Filtering + +You can filter the requests by their type by clicking on the filter buttons at the top of the network tab. +You can also filter by route as well + ## Request info Clicking on any request name will show you detailed information about that request. This includes the request's name, the diff --git a/docs/content/02-features/05-settings-tab.mdx b/docs/content/02-features/05-settings-tab.mdx deleted file mode 100644 index 00bde8a3..00000000 --- a/docs/content/02-features/05-settings-tab.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: "Settings Tab" -summary: "The Settings tab lets you customize panel position, size, expansion level, triggers, URL flags, and route boundary gradient for React Router Devtools." -description: "Detailed overview of all features on the Settings Tab." ---- -The settings tab is where you can override the default settings for your project. - - -## Position - -This option is used to set the position of the React Router Devtools trigger (the button that opens the panel). The possible values are: -- `top-left` - the trigger will be positioned at the top left corner of the screen -- `top-right` - the trigger will be positioned at the top right corner of the screen -- `bottom-left` - the trigger will be positioned at the bottom left corner of the screen -- `bottom-right` - the trigger will be positioned at the bottom right corner of the screen -- `middle-left` - the trigger will be positioned at the middle left of the screen -- `middle-right` - the trigger will be positioned at the middle right of the screen - -## Default Open - -This option is used to set the initial state of the panel. If set to `true` the panel will be open by default, if set to `false` -the panel will be closed by default. - -## Expansion Level - -This option is used to set the initial expansion level of the returned JSON data in the **Active Page** tab. By default it is set to -0 and if you open up the **Active Page** and look at the returned loader data it will look like this: - -```ts -"data": { ... } + -``` - -If you set the expansion level to 1 the returned loader data will look like this: - -```ts -"data": { - "property": "value" -} -``` - -## Height - -This option is used to set the initial height of the panel. The default value is 400px. - -## Min Height - -This option is used to set the minimum height of the panel. The default value is 200px. - -## Max Height - -This option is used to set the maximum height of the panel. The default value is 800px. - -## Hide Until Hover - -This option is used to set whether the trigger should be hidden until you hover over it. The default value is `false`. - -## Panel Location - -This option is used to set the location of the panel. The possible values are: -- `top` - the panel will be positioned at the top of the screen -- `bottom` - the panel will be positioned at the bottom of the screen - -## Require URL Flag - -This option is used to set whether the panel should be opened only if the URL contains a specific flag. The default value is `false`. - - -If you set this option to `true` and you forget to set the URL flag, the panel will hide and you will not be able to see it -until you enter the url flag. - -The default one is `rdt=true` and if you set this option to `true` you will have to add `?rdt=true` to the URL in order to see the panel. - - -## URL Flag - -This option is used to set the URL flag that is required to open the panel. The default value is `rdt`. - -You can set it to whatever you wish and if you set the **Require URL Flag** option to `true` you will have to add `?yourFlag=true` to the URL in order to see the panel. - -## Route Boundary Gradient - -This option is used to set the color of the route boundary gradient. The possible values are: -- `sea` -- `hyper` -- `gotham` -- `gray` -- `watermelon` -- `ice` -- `silver` - - -This changes the color of the route boundary gradient in the **Active Page** tab. When you hover over any route in the panel it will show you it's boundaries. - - -The default value is `ice`. diff --git a/docs/content/02-features/06-detach.mdx b/docs/content/02-features/06-detach.mdx deleted file mode 100644 index 42210d9e..00000000 --- a/docs/content/02-features/06-detach.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Detached mode" -summary: "Detached mode lets you pop out the Devtools panel into a separate window that stays in sync with the main panel." -description: "How you can detach your panel into a new window" ---- - -There is a special button on the bottom left side of the panel above the X button. -When you click on it, the panel will detach and open in a new window. - -The detached window will keep in sync with the main panel and will show the same content. -The logs on the server will happen twice, once for the main panel and once for the detached window. - -When you close the detached window, or the main panel, the other one will be terminated and closed. -In case the detached mode hangs for some reason, you can always return it to the main site by -clicking the trigger while in detached mode. \ No newline at end of file diff --git a/docs/content/03-configuration/01-client.mdx b/docs/content/03-configuration/01-client.mdx index 6be86cbc..6fd78cc1 100644 --- a/docs/content/03-configuration/01-client.mdx +++ b/docs/content/03-configuration/01-client.mdx @@ -1,78 +1,29 @@ --- title: React Router Devtools Client Configuration -summary: "The client configuration lets you customize React Router Devtools behavior, including panel position, size, URL flags, live URLs, breakpoints, and route boundary settings." +summary: "The client configuration lets you customize React Router Devtools behavior through the Vite plugin, including expansion level and route boundary gradient settings." description: Configuration options for the React Router Devtools client --- - - -All of the following options can be set in the dev tools panel **"Settings page"** and they override the default ones. Your preferences are -stored in localStorage and if they do not exist there the default ones are used. - - Before we explain all the possible options here is the client configuration Typescript type: ```ts type RdtClientConfig = { - position: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "middle-left" | "middle-right"; - liveUrls: { name: string, url: string }[]; - liveUrlsPosition: "top-left" | "top-right" | "bottom-left" | "bottom-right"; - defaultOpen: boolean; expansionLevel: number; - height: number; - minHeight: number; - maxHeight: number; - hideUntilHover: boolean; - panelLocation: "top" | "bottom"; - requireUrlFlag: boolean; - urlFlag: string; - breakpoints: {name: string, min: number, max: number }[], routeBoundaryGradient: "sea" | "hyper" | "gotham" | "gray" | "watermelon" | "ice" | "silver"; - showBreakpointIndicator: boolean; - showRouteBoundariesOn: "hover" | "click"; } ``` -Let's go through each option and see what it does. - -## Live URLs - -This option is used to set the live urls that will be displayed in the bottom left corner of the screen. The default value is an empty array. -It allows you to specify multiple live urls that you can use to open the current page in a new tab. - -## Live URLs position - -This option is used to set the position of the live urls that will be displayed in the bottom left corner of the screen. The possible values are: -- `top-left` - the live urls will be positioned at the top left corner of the screen -- `top-right` - the live urls will be positioned at the top right corner of the screen -- `bottom-left` - the live urls will be positioned at the bottom left corner of the screen -- `bottom-right` - the live urls will be positioned at the bottom right corner of the screen - -## Position - -This option is used to set the position of the React Router Devtools trigger (the button that opens the panel). The possible values are: -- `top-left` - the trigger will be positioned at the top left corner of the screen -- `top-right` - the trigger will be positioned at the top right corner of the screen -- `bottom-left` - the trigger will be positioned at the bottom left corner of the screen -- `bottom-right` - the trigger will be positioned at the bottom right corner of the screen -- `middle-left` - the trigger will be positioned at the middle left of the screen -- `middle-right` - the trigger will be positioned at the middle right of the screen - -## Default Open + +Configuration can only be set through the Vite plugin. The settings are applied when the devtools are loaded. + -This option is used to set the initial state of the panel. If set to `true` the panel will be open by default, if set to `false` -the panel will be closed by default. +Let's go through each option and see what it does. ## Expansion Level -This option is used to set the initial expansion level of the returned JSON data in the **Active Page** tab. By default it is set to -1 and if you open up the **Active Page** and look at the returned loader data it will look like this: - -```ts -"data": { ... } + -``` +This option is used to set the initial expansion level of the returned JSON data in the **Active Page** tab. The default value is `1`. -If you set the expansion level to 1 the returned loader data will look like this: +If you open up the **Active Page** and look at the returned loader data with expansion level set to 1, it will look like this: ```ts "data": { @@ -80,44 +31,11 @@ If you set the expansion level to 1 the returned loader data will look like this } ``` -## Height - -This option is used to set the initial height of the panel. The default value is 400px. - -## Min Height - -This option is used to set the minimum height of the panel. The default value is 200px. - -## Max Height - -This option is used to set the maximum height of the panel. The default value is 800px. - -## Hide Until Hover +If you set the expansion level to 0, the returned loader data will be collapsed: -This option is used to set whether the trigger should be hidden until you hover over it. The default value is `false`. - -## Panel Location - -This option is used to set the location of the panel. The possible values are: -- `top` - the panel will be positioned at the top of the screen -- `bottom` - the panel will be positioned at the bottom of the screen - -## Require URL Flag - -This option is used to set whether the panel should be opened only if the URL contains a specific flag. The default value is `false`. - - -If you set this option to `true` and you forget to set the URL flag, the panel will hide and you will not be able to see it -until you enter the url flag. - -The default one is `rdt=true` and if you set this option to `true` you will have to add `?rdt=true` to the URL in order to see the panel. - - -## URL Flag - -This option is used to set the URL flag that is required to open the panel. The default value is `rdt`. - -You can set it to whatever you wish and if you set the **Require URL Flag** option to `true` you will have to add `?yourFlag=true` to the URL in order to see the panel. +```ts +"data": { ... } + +``` ## Route Boundary Gradient @@ -131,31 +49,10 @@ This option is used to set the color of the route boundary gradient. The possibl - `silver` -This changes the color of the route boundary gradient in the **Active Page** tab. When you hover over any route in the panel it will show you it's boundaries. +This changes the color of the route boundary gradient in the **Active Page** tab. When you click the "Show Route Boundary" button on a route segment in the panel, it will highlight the route's boundaries with this gradient. -The default value is `ice`. - -## Breakpoints - -This option allows you to define custom breakpoints that show in the bottom left corner of the panel to help you determine the current screen breakpoint you have defined. -By default the breakpoints are set to tailwind breakpoints but you can change them to whatever you want. - -Eg: -```ts -breakpoints: [{name: "lg", min: 0, max: 768}, {name: "xl", min: 768, max: 1024}, {name: "2xl", min: 1024, max: Infinity}], -``` - -## Show breakpoint indicator - -This option allows you to show/hide the current breakpoint in the bottom left corner of the panel. - -## Show route boundaries on - -This option allows you to either show route boundaries when you hover a route segment on the pages tab or -it shows a dedicated button called "Show Route Boundary" that shows the route boundary for that route on click. - -Default value is `click`; +The default value is `watermelon`. ## Creating a custom configuration @@ -166,19 +63,8 @@ To create a custom configuration you can use the following code snippet: const customConfig = defineRdtConfig({ client: { - position: "top-right", - defaultOpen: true, - expansionLevel: 1, - height: 500, - minHeight: 300, - maxHeight: 1000, - hideUntilHover: true, - panelLocation: "bottom", - requireUrlFlag: true, - urlFlag: "customFlag", + expansionLevel: 2, routeBoundaryGradient: "gotham", - breakpoints: [{name: "lg", min: 0, max: 768}, {name: "xl", min: 768, max: 1024}, {name: "2xl", min: 1024, max: Infinity}], - showBreakpointIndicator: false } }); @@ -195,7 +81,3 @@ export default defineConfig({ plugins: [reactRouterDevTools(customConfig)], }); ``` - - - Try opening up the dev tools panel deployed on this site and playing around with the settings in the settings tab! - diff --git a/docs/content/03-configuration/02-editor.mdx b/docs/content/03-configuration/02-editor.mdx deleted file mode 100644 index f6223211..00000000 --- a/docs/content/03-configuration/02-editor.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: React Router Devtools Editor Configuration -summary: "Configure React Router Devtools to open files in your preferred editor by defining a custom editor name and open function, with examples for VS Code, WebStorm, and GoLand." -description: Configuration options for the React Router Devtools to interface with your editor ---- - -Everyone uses their own editors, so it's important to be able to configure the editor that React Router Devtools will open your files in. - -```ts -type EditorConfig = { - name: string; - open(path: string, lineNumber: string | undefined): void; -} -``` - -## `name` - -The name of the editor that will be displayed on the Open in Editor button. - -## `open` - -This function will be called when the user clicks the Open in Editor button. It will receive the path to the file and the line number to open the file at. -This function will both handle the case where you shift + right click an element on the page AND the open in X button in the UI, they return different values -so be sure to cover both of them. - -```ts -import { exec } from "node:child_process"; -import { normalizePath } from "vite"; - -function open(path: string, lineNumber: string | undefined) { - exec(`code -g "${normalizePath(path)}${lineNumber ? `:${lineNumber}` : ""}"`); -} -``` - -## Editors - -Below are some examples of configurations for popular editors. - -### VS Code - -To use VS Code as your editor, you don't need to do anything, it's the default editor. - -### WebStorm - -To use WebStorm as your editor, you can use the following configuration: - -```ts -import { exec } from "node:child_process"; -import { cwd } from "node:process"; - -const editor = { - name: "WebStorm", - open(path, lineNumber) { - exec( - `webstorm "${process.cwd()}/${path}" --line ${lineNumber ? `--line ${lineNumber}` : ""}`.replace( - /\$/g, - "\\$", - ), - ); - }, -}; -``` - -### GoLand - -To use GoLand as your editor, you can use the following configuration: - -```ts -import { exec } from "node:child_process"; -import { cwd } from "node:process"; - -const editor = { - name: "GoLand", - open(path, lineNumber) { - if (!path) return; - exec( - `goland "${process.cwd()}/${path}" ${lineNumber ? `--line ${lineNumber}` : ""}` - ); - }, -}; -``` \ No newline at end of file diff --git a/docs/content/03-configuration/02-server.mdx b/docs/content/03-configuration/02-server.mdx new file mode 100644 index 00000000..f41c83f8 --- /dev/null +++ b/docs/content/03-configuration/02-server.mdx @@ -0,0 +1,336 @@ +--- +title: React Router Devtools Server Configuration +summary: "Configure React Router Devtools server logging, including options for silence, cookies, deferred data, actions, loaders, middleware, cache, site clears, and server timings." +description: Configuration options for the React Router Devtools server +--- + +As with the client configuration, we will first see the full configuration type: + +```ts +interface DevToolsServerConfig { + silent?: boolean; + serverTimingThreshold?: number; + logs?: { + cookies?: boolean; + defer?: boolean; + actions?: boolean; + loaders?: boolean; + cache?: boolean; + siteClear?: boolean; + serverTimings?: boolean; + middleware?: boolean; + }; +} +``` + +## `silent` + +When `true`, the server will not log anything to the console. This is useful for production environments or when you want to completely disable server-side logging. + +**Default:** `false` (in development), `true` (in production) + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + server: { + silent: true, // Disable all server logs + } + }), + reactRouter(), + ], +}); +``` + + +The `silent` option is a global toggle. If you want granular control over specific log types, leave `silent` as `false` and configure individual log options in the `logs` object. + + +## `serverTimingThreshold` + +This option sets the threshold (in milliseconds) for server timings to be logged in the console with different colors. + +- If the server timing is **greater than** this threshold, it will be logged in **red** (indicating potential performance issues) +- If the server timing is **less than or equal to** this threshold, it will be logged in **green** + +**Default:** `Number.POSITIVE_INFINITY` (all timings shown in green) + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + server: { + serverTimingThreshold: 100, // Timings > 100ms shown in red + } + }), + reactRouter(), + ], +}); +``` + +## `logs` + +This object allows you to configure individual server log types. Each key is a log type and the value is a boolean indicating whether to log that type. + +**All log types are `true` by default**, so you don't have to provide anything. If you want granular control, you can selectively disable specific log types. Alternatively, use the `silent` option to turn off all logs at once. + +### `cookies` + +When `true`, the server will log all cookies sent by the server in the `Set-Cookie` header. + +**Default:** `true` + +**What gets logged:** +- Cookie names and values +- Cookie attributes (domain, path, secure, httpOnly, etc.) +- When cookies are set or modified + +**Example:** +```ts +logs: { + cookies: false, // Disable cookie logging +} +``` + +### `defer` + +When `true`, the server will log all deferred actions and loaders. + +**Default:** `true` + +**What gets logged:** +- The location where defer was called +- The keys that were deferred +- The time it took for each deferred promise to resolve +- Any errors that occurred during deferred resolution + +**Example:** +```ts +logs: { + defer: true, // Log deferred data +} +``` + +### `actions` + +When `true`, the server will log all action functions that are executed. + +**Default:** `true` + +**What gets logged:** +- Action route ID +- Request method (POST, PUT, DELETE, etc.) +- Execution time +- Return status + +**Example:** +```ts +logs: { + actions: true, // Log all actions +} +``` + +### `loaders` + +When `true`, the server will log all loader functions that are executed. + +**Default:** `true` + +**What gets logged:** +- Loader route ID +- Request URL +- Execution time +- Return status + +**Example:** +```ts +logs: { + loaders: true, // Log all loaders +} +``` + +### `middleware` + +When `true`, the server will log all middleware function executions. + +**Default:** `true` + +**What gets logged:** +- Middleware function route ID +- Execution context +- Execution time +- Any errors or return values + +**Example:** +```ts +logs: { + middleware: true, // Log middleware calls +} +``` + + +Middleware logging is particularly useful for debugging request processing pipelines and understanding the order of middleware execution. + + +### `cache` + +When `true`, the server will log all loaders and actions that return a `Cache-Control` header. + +**Default:** `true` + +**What gets logged:** +- Route ID +- Cache-Control header value +- Cache strategy (max-age, s-maxage, etc.) +- Cache directives (public, private, no-cache, etc.) + +**Example:** +```ts +logs: { + cache: true, // Log cache headers +} +``` + +### `siteClear` + +When `true`, the server will log when the site cache is cleared or when any response includes the `Clear-Site-Data` header. + +**Default:** `true` + +**What gets logged:** +- Routes that triggered cache clearing +- Clear-Site-Data header values +- Types of data being cleared (cache, cookies, storage, etc.) + +**Example:** +```ts +logs: { + siteClear: true, // Log cache clear events +} +``` + +### `serverTimings` + +When `true`, the server will log all server timings that are returned with a request via the `Server-Timing` header. + +**Default:** `true` + +**What gets logged:** +- Timing metric names +- Duration values +- Descriptions +- Color-coded based on `serverTimingThreshold` + +**Example:** +```ts +logs: { + serverTimings: true, // Log server timing headers +} +``` + +## Complete Configuration Example + +Here's a complete example showing all server configuration options: + +```ts +import { defineConfig } from 'vite' +import { reactRouter } from '@react-router/dev/vite' +import { reactRouterDevTools } from "react-router-devtools" + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + server: { + // Global toggle - turns off ALL logging + silent: false, + + // Server timing threshold (in milliseconds) + // Timings above this will be logged in red + serverTimingThreshold: 100, + + // Granular log configuration + logs: { + cookies: true, // Log Set-Cookie headers + defer: true, // Log deferred loaders/actions + actions: true, // Log action executions + loaders: true, // Log loader executions + middleware: true, // Log middleware executions + cache: true, // Log Cache-Control headers + siteClear: true, // Log Clear-Site-Data headers + serverTimings: true, // Log Server-Timing headers + } + } + }), + reactRouter(), + ], +}) +``` + +## Production Configuration + +For production environments, you typically want to disable all server logging: + +```ts +import { reactRouterDevTools } from "react-router-devtools" + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + server: { + silent: true, // Disable all logs in production + }, + includeInProd: { + server: true, // Include server utilities but with silent logging + } + }), + reactRouter(), + ], +}) +``` + +Alternatively, you can set `process.rdt_config` at runtime in production: + +```ts +// In your server entry file +if (process.env.NODE_ENV === 'production') { + process.rdt_config = { + silent: true, + } +} +``` + +## Selective Logging Example + +Enable only specific log types you care about: + +```ts +import { reactRouterDevTools } from "react-router-devtools" + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + server: { + silent: false, + serverTimingThreshold: 50, // Flag slow operations > 50ms + logs: { + cookies: false, // Don't log cookies + defer: true, // Log deferred data + actions: true, // Log actions + loaders: false, // Don't log loaders (too noisy) + middleware: true, // Log middleware + cache: true, // Log cache headers + siteClear: false, // Don't log cache clears + serverTimings: true, // Log performance timings + } + } + }), + reactRouter(), + ], +}) +``` \ No newline at end of file diff --git a/docs/content/03-configuration/03-general.mdx b/docs/content/03-configuration/03-general.mdx new file mode 100644 index 00000000..7a603d04 --- /dev/null +++ b/docs/content/03-configuration/03-general.mdx @@ -0,0 +1,283 @@ +--- +title: React Router Devtools General Configuration +summary: "General configuration covers plugin directory setup, TanStack DevTools integration, and controlling whether client or server parts of React Router Devtools are included in production." +description: General Configuration options for the React Router Devtools +--- + +This covers the general configuration options for the React Router Devtools. + +## General Config Type + +```ts +type ReactRouterViteConfig = { + client?: Partial + server?: DevToolsServerConfig + pluginDir?: string + includeInProd?: { + client?: boolean + server?: boolean + devTools?: boolean + } + tanstackConfig?: Omit, "customTrigger"> + tanstackClientBusConfig?: Partial + tanstackViteConfig?: TanStackDevtoolsViteConfig +} +``` + + +You can find more info on TanStack specific configuration options here: +https://tanstack.com/devtools/latest/docs/overview + + +## `pluginDir` + +The relative path to your plugin directory. If you have a directory for react-router-devtools plugins you can point to it and they +will be automatically imported and added to the dev tools. + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + pluginDir: "./plugins" + }), + reactRouter(), + ], +}); +``` + +## `includeInProd` + +This option is used to set whether parts of the plugin should be included in production builds or not. + +By default, all parts are excluded from production builds. You can selectively include different parts: + +- **`client`**: Include the devtools UI in production +- **`server`**: Include server-side logging and utilities in production +- **`devTools`**: Include TanStack DevTools integration in production + +```ts +includeInProd?: { + client?: boolean // Default: false + server?: boolean // Default: false + devTools?: boolean // Default: false +} +``` + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + includeInProd: { + client: false, + server: true, // Include server logs in production + devTools: false + }, + }), + reactRouter(), + ], +}); +``` + + +If you decide to deploy parts to production you should be very careful that you don't expose the dev tools to your clients or anybody +who is not supposed to see them. + +**Important notes:** +- The server part uses chalk which might not work in non-node environments +- If you wish to edit the plugin server config in production, you can set `process.rdt_config` to an object with the same shape as the config object and it will be used instead of the default production config (`{ silent: true }`) +- Consider adding authentication/authorization checks before exposing devtools in production + + +## TanStack DevTools Integration + +React Router Devtools v6+ integrates with TanStack DevTools, providing enhanced debugging capabilities. You can configure TanStack-specific behavior through these options: + +### `tanstackConfig` + +Configure the TanStack DevTools behavior and appearance. + +```ts +tanstackConfig?: Omit, "customTrigger"> +``` + + +The `customTrigger` option is automatically managed by React Router Devtools and cannot be configured manually. + +All the options are available here: +https://tanstack.com/devtools/latest/docs/configuration + + +**Common options:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + tanstackConfig: { + position: "bottom-right", // Position of the devtools trigger + // Add other TanStack config options as needed + } + }), + reactRouter(), + ], +}); +``` + + +### `tanstackClientBusConfig` + +Configure the TanStack client event bus for advanced use cases. + +```ts +tanstackClientBusConfig?: Partial +``` + +This is an advanced option for customizing how TanStack DevTools communicates between client and server. Most users won't need to configure this. + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + tanstackClientBusConfig: { + // Advanced event bus configuration + } + }), + reactRouter(), + ], +}); +``` + +### `tanstackViteConfig` + +Configure the TanStack Vite plugin directly for advanced scenarios. + +```ts +tanstackViteConfig?: TanStackDevtoolsViteConfig +``` + +This allows you to pass configuration directly to the underlying TanStack DevTools Vite plugin. + +**Example:** +```ts +import { reactRouterDevTools } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + tanstackViteConfig: { + // TanStack Vite plugin configuration + } + }), + reactRouter(), + ], +}); +``` + + +You can find more info on TanStack specific configuration options here: +https://tanstack.com/devtools/latest/docs/plugin-configuration + + +## Complete Configuration Example + +Here's a complete example showing all general configuration options: + +```ts +import { defineConfig } from 'vite' +import { reactRouter } from '@react-router/dev/vite' +import { reactRouterDevTools, defineRdtConfig } from "react-router-devtools" + +const rdtConfig = defineRdtConfig({ + // Client configuration + client: { + expansionLevel: 2, + routeBoundaryGradient: "gotham", + }, + + // Server configuration + server: { + silent: false, + logs: { + loaders: true, + actions: true, + } + }, + + // Plugin directory + pluginDir: "./plugins", + + // Production inclusion + includeInProd: { + client: false, + server: true, + devTools: false, + }, + + // TanStack DevTools configuration + tanstackConfig: { + position: "bottom-right", + buttonPosition: "bottom-right", + }, + + // TanStack client bus configuration (advanced) + tanstackClientBusConfig: { + // Custom event bus config if needed + }, + + // TanStack Vite plugin configuration (advanced) + tanstackViteConfig: { + // Custom Vite plugin config if needed + } +}) + +export default defineConfig({ + plugins: [ + reactRouterDevTools(rdtConfig), + reactRouter(), + ], +}) +``` + +## Using `defineRdtConfig` + +The `defineRdtConfig` helper provides type safety for your configuration: + +```ts +import { defineRdtConfig } from "react-router-devtools" + +const config = defineRdtConfig({ + // Your configuration with full TypeScript support + client: { + expansionLevel: 2, + }, + tanstackConfig: { + position: "bottom-right", + } +}) + +export default config +``` + +You can then import and use this configuration in your `vite.config.ts`: + +```ts +import { reactRouterDevTools } from "react-router-devtools" +import rdtConfig from "./rdt.config" + +export default defineConfig({ + plugins: [ + reactRouterDevTools(rdtConfig), + reactRouter(), + ], +}) +``` diff --git a/docs/content/03-configuration/03-server.mdx b/docs/content/03-configuration/03-server.mdx deleted file mode 100644 index 3baba0b9..00000000 --- a/docs/content/03-configuration/03-server.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: React Router Devtools Server Configuration -summary: "Configure React Router Devtools server logging, including options for silence, cookies, deferred data, actions, loaders, cache, site clears, and server timings." -description: Configuration options for the React Router Devtools server ---- - - -As with the client configuration, we will first see the full configuration type: - -```ts -interface ReactRouterServerConfig { - silent?: boolean; - logs?: { - cookies?: boolean; - defer?: boolean; - actions?: boolean; - loaders?: boolean; - cache?: boolean; - siteClear?: boolean; - serverTimings?: boolean; - }; -} -``` - -## `silent` - -When `true`, the server will not log anything to the console. This is useful for production environments. - -## `logs` - -This object allows you to configure the server logs. Each key is a log type and the value is a boolean indicating whether to log that type. -All are `true` by default so you don't have to provide anything, if you want to be granular you can, otherwise you can use the `silent` option to turn off -all logs. - -### `cookies` - -When `true`, the server will log all cookies sent by the server in the "Set-Cookie" header. - -### `defer` - -When `true`, the server will log all deferred actions. -The following gets logged: -- The defer location -- The keys that were deferred -- The time it took for each key to resolve - -### `actions` - -When `true`, the server will log all actions that are hit with a request. - -### `loaders` - -When `true`, the server will log all loaders that are hit with a request. - -### `cache` - -When `true`, the server will log all loaders/actions that return a `Cache Control` header. - -### `siteClear` - -When `true`, the server will log when the site cache is cleared, or anything else with the `Clear-Site-Data` header. - -### `serverTimings` - -When `true`, the server will log all server timings that are returned with a request - -## `serverTimingThreshold` - -This option is used to set the threshold for server timings to be logged in the console. -If the server timing is greater than this threshold, it will be logged in red, otherwise it will be logged in green. - -By default it is set to `Number.POSITIVE_INFINITY` which means that all server timings will be logged in green. \ No newline at end of file diff --git a/docs/content/03-configuration/04-general.mdx b/docs/content/03-configuration/04-general.mdx deleted file mode 100644 index bac39817..00000000 --- a/docs/content/03-configuration/04-general.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: React Router Devtools General Configuration -summary: "General configuration covers plugin directory setup, enabling enhanced logs, and controlling whether client or server parts of React Router Devtools are included in production." -description: General Configuration options for the React Router Devtools to interface with your editor ---- - - -This covers the general configuration options for the React Router Devtools. - -## General Config - -```ts -type GeneralConfig = { - pluginDir?: string - includeInProd?: { - client?: boolean - server?: boolean - } - -} -``` -## enhancedLogs - -This configuration flag enables/disables enhanced logs feature. - -## `pluginDir` - -The relative path to your plugin directory. If you have a directory for react-router-devtools plugins you can point to it and they -will be automatically imported and added to the dev tools. - -## `includeInProd` - -This option is used to set whether the plugin should be included in production builds or not. - -By default it is set to `undefined` and if you set this option to an object with the `client`, `context` and `server` properties set to `true` the plugin will be included in production builds. - -The client part includes the dev tools with the plugin and the server part includes the info logs. You can granularly configure the -exact behavior of both sides with client and server configs respectively. - - -Each of these flags will include a part of the plugin in production, in order for any of these to work `react-router-devtools` need to be switched over to -a regular dependency and included in your project. If you only want to include the `devTools` helper in production, for example, you can -set `includeInProd` to `{ devTools: true }` and the `devTools` part will be included in production and available always. - - - If you decide to deploy parts to production you should be very careful that you don't expose the dev tools to your clients or anybody - who is not supposed to see them. Also the server part uses chalk which might not work in non-node environments! - - Also, if you wish to edit the plugin server config in production you can set `process.rdt_config` to an object with the same shape as the - config object and it will be used instead of the default production config (`{ silent: true }`). - - - ```ts - import { reactRouterDevTools } from "react-router-devtools"; - - export default defineConfig({ - plugins: [ - reactRouterDevTools({ - includeInProd: { - client: true, - server: true, - devTools: true - }, - }), - ], - }); - ``` diff --git a/docs/content/04-guides/01-migration.mdx b/docs/content/04-guides/01-migration.mdx index ac3774c5..ecb6f3c1 100644 --- a/docs/content/04-guides/01-migration.mdx +++ b/docs/content/04-guides/01-migration.mdx @@ -1,26 +1,196 @@ --- title: "Migration guide" -summary: "Learn how to migrate from remix-development-tools to React Router Devtools by updating your vite.config.ts plugin setup." -description: "Migration guide from remix-development-tools" +summary: "Learn how to migrate from react-router-devtools v5 to v6 " +description: "Migration guide from react-router-devtools v5 to v6" --- +## Migration from react-router-devtools v5 to v6 + ### vite.config.ts -If you're migrating your `remix-development-tools` from v4.x to react-router v7 and you were already running it as -a Vite plugin here is all you need to do: +If you're migrating your `react-router-devtools` from v5 to v6 and you were already running it as +a Vite plugin, a lot of the configuration has been offloaded to TanStack DevTools. So you will +need to update your `vite.config.ts` file to include the new configuration options. + +Another thing that has changed is the way plugins are configured. In v6, instead of exporting a function +component like so: +```tsx +export const yourPlugin = () => ({ + name: "your-plugin", + component: YourPluginComponent +}) +``` + +You now export it as a plain object like the following, and also the `component` has been changed to `render`: +```tsx +export const yourPlugin = { + name: "your-plugin", + render: YourPluginComponent +} +``` + + +We use the following plugin documentation so you can export your plugins like this: +https://tanstack.com/devtools/latest/docs/third-party-plugins + + +And that's it! You should be good to go. If you have any issues, please reach out. + +### Breaking Changes + +#### 1. TanStack DevTools Integration + +React Router Devtools v6 now integrates with [TanStack DevTools](https://tanstack.com/devtools), providing enhanced debugging capabilities and a more powerful devtools experience. -```diff -import { defineConfig } from 'vite'; -- import { vitePlugin as remix } from '@remix-run/dev'; -+ import { reactRouter } from '@react-router/dev/vite'; -- import { remixDevTools } from 'remix-development-tools' -+ import { reactRouterDevTools } from 'react-router-devtools' +**What this means:** +- The devtools UI now includes TanStack DevTools panels alongside React Router specific features +- You can now configure TanStack-specific behavior through the plugin configuration +- Enhanced query and state inspection capabilities + +#### 2. Configuration Structure Changes + +The configuration API has been expanded to support TanStack integration: + +**Before (v5):** +```ts +import { reactRouterDevTools, defineRdtConfig } from "react-router-devtools"; export default defineConfig({ -- plugins: [remixDevTools(), remix()], -+ plugins: [reactRouterDevTools(), reactRouter()], -}) + plugins: [ + reactRouterDevTools({ + client: { + expansionLevel: 2, + routeBoundaryGradient: "gotham", + position: "bottom-right", // Position of the devtools trigger + + }, + server: { + silent: false, + logs: { + loaders: true, + actions: true, + } + }, + pluginDir: "./plugins", + includeInProd: { + client: false, + server: false, + } + }), + reactRouter(), + ], +}); +``` + +**After (v6):** +```ts +import { reactRouterDevTools, defineRdtConfig } from "react-router-devtools"; + +export default defineConfig({ + plugins: [ + reactRouterDevTools({ + // Client configuration (unchanged) + client: { + expansionLevel: 2, + routeBoundaryGradient: "gotham", + }, + // Server configuration (unchanged) + server: { + silent: false, + logs: { + loaders: true, + actions: true, + } + }, + // Plugin directory (unchanged) + pluginDir: "./plugins", + // Production inclusion (expanded) + includeInProd: { + client: false, + server: false, + devTools: false, // NEW: Control TanStack DevTools in production + }, + // NEW: TanStack DevTools configuration + tanstackConfig: { + position: "bottom-right", + // Other TanStack config options + }, + // NEW: TanStack event bus configuration + tanstackClientBusConfig: { + // Custom event bus config + }, + // NEW: TanStack Vite plugin configuration + tanstackViteConfig: { + // Custom Vite plugin config for TanStack + } + }), + reactRouter(), + ], +}); +``` + +### New Configuration Options + +#### `tanstackConfig` + +Configure the TanStack DevTools behavior and appearance: + +```ts +tanstackConfig?: { + position?: "top-left" | "top-right" | "bottom-left" | "bottom-right" + + // Additional TanStack configuration options +} +``` + + +The `customTrigger` option is automatically set and cannot be configured as React Router Devtools manages the trigger internally. + +The full config is here: +https://tanstack.com/devtools/latest/docs/configuration + + +#### `tanstackClientBusConfig` + +Configure the TanStack client event bus for advanced use cases: + +```ts +tanstackClientBusConfig?: { + // ClientEventBusConfig options from @tanstack/devtools +} +``` + +#### `tanstackViteConfig` + +Configure the TanStack Vite plugin directly: + +```ts +tanstackViteConfig?: { + // TanStackDevtoolsViteConfig options from @tanstack/devtools-vite +} +``` + +#### `includeInProd.devTools` + +New option to control whether TanStack DevTools are included in production builds: + +```ts +includeInProd: { + client: false, + server: false, + devTools: false, // NEW: Control TanStack DevTools inclusion +} +``` + +You also need to set `removeDevtoolsOnBuild` to `false` in the `tanstackViteConfig` if you want to include TanStack DevTools in production: + +```ts +tanstackViteConfig: { + removeDevtoolsOnBuild: false, // Set to false to include in production +} ``` +### Plugin System Changes -And that's it! You should be good to go. If you have any issues, please reach out to us. \ No newline at end of file +The plugin system remains largely unchanged, but plugins now have access to enhanced capabilities through TanStack integration. +The only changes are the rename from `component` to `render` and exporting plugins as plain objects instead of functions. diff --git a/docs/content/04-guides/03-hydrogen-oxygen.mdx b/docs/content/04-guides/03-hydrogen-oxygen.mdx index 8ef9bd62..d311b249 100644 --- a/docs/content/04-guides/03-hydrogen-oxygen.mdx +++ b/docs/content/04-guides/03-hydrogen-oxygen.mdx @@ -66,8 +66,6 @@ export default defineConfig({ + ssr: { + optimizeDeps: { + include: [ -+ 'beautify', -+ 'react-diff-viewer-continued', + 'react-d3-tree', + ], + }, diff --git a/docs/content/_index.mdx b/docs/content/_index.mdx index cb961844..3f54a89a 100644 --- a/docs/content/_index.mdx +++ b/docs/content/_index.mdx @@ -1,7 +1,7 @@ --- title: "Quick Start" summary: "Get up and running with React Router Devtools in a React Router 7+ project using Vite and ESM." -description: "Learn the prerequisites and benefits of using React Router Devtools, including loader data display, route visualization, error and hydration tracking, server logs, and route boundaries." +description: "Learn the prerequisites and benefits of using React Router Devtools, including TanStack integration, loader data display, route visualization, error and hydration tracking, server logs, and route boundaries." --- This documentation covers everything you need to know to get started with `react-router-devtools`. @@ -22,12 +22,16 @@ To avoid creating user confusion and giving you a subpar experience, we have dec ## Why use `react-router-devtools`? -`react-router-devtools` is a set of tools that help you to develop your React Router application. +`react-router-devtools` is a comprehensive set of tools that help you develop and debug your React Router application. -They help you, but are not limited to, to do the following things: +Starting from v6, it integrates [TanStack DevTools](https://tanstack.com/devtools) for enhanced debugging capabilities alongside React Router specific features. + +Key features include: +- **TanStack DevTools Integration** - Enhanced state inspection and debugging capabilities powered by TanStack - **Loader data display** - You can see the data that is being loaded by your loaders. - **Route display** - You can see the routes that are being used by your application in list/tree format. - **Error tracking** - You can see invalid HTML rendered on your page and where it's coming from. - **Hydration mismatch tracking** - You can see if there are any hydration mismatches in your application, what was rendered on the client and what was rendered on the server. - **Server logs** - You can see the logs of your server in the browser. - **Route boundaries** - You can see the route boundaries by hovering over elements. +- **Network tracing** - Trace and analyze your loader and action execution times with detailed timing information. diff --git a/docs/package.json b/docs/package.json index 24e9d3b5..7c231f1a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -36,12 +36,12 @@ "@content-collections/remix-vite": "0.2.2", "@epic-web/client-hints": "1.3.5", "@forge42/seo-tools": "1.3.0", - "@react-router/node": "^7.5.3", + "@react-router/node": "7.9.5", "@tsparticles/engine": "^3.3.0", "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.3.0", "clsx": "2.1.1", - "framer-motion": "^11.0.8", + "framer-motion": "^12.23.24", "hono": "4.6.20", "i18next": "24.2.2", "i18next-browser-languagedetector": "8.0.2", @@ -51,7 +51,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "15.4.0", - "react-router": "^7.5.3", + "react-router": "^7.9.3", "react-router-hono-server": "2.10.0", "rehype-slug": "6.0.0", "remix-hono": "0.0.18", @@ -64,11 +64,11 @@ "devDependencies": { "@babel/preset-typescript": "7.26.0", "@dotenvx/dotenvx": "1.34.0", - "@react-router/dev": "^7.5.3", + "@react-router/dev": "7.9.5", "@tailwindcss/typography": "0.5.16", "@tailwindcss/vite": "^4.1.4", - "@testing-library/react": "16.2.0", - "@types/node": "22.13.1", + "@testing-library/react": "16.3.0", + "@types/node": "24.10.0", "@types/prompt": "1.1.9", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", @@ -78,16 +78,16 @@ "@vitest/coverage-v8": "3.0.5", "@vitest/ui": "3.0.5", "babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124", - "chalk": "5.4.1", - "happy-dom": "16.8.1", + "chalk": "5.6.2", + "happy-dom": "20.0.10", "npm-run-all": "4.1.5", "playwright": "1.50.1", "prompt": "1.3.0", "react-router-devtools": "*", "tailwindcss": "4.0.9", - "tsx": "4.19.2", - "typescript": "^5.8.3", - "vite": "^6.3.3", + "tsx": "4.20.6", + "typescript": "^5.9.3", + "vite": "^7.2.2", "vite-plugin-babel": "1.3.0", "vite-plugin-icons-spritesheet": "3.0.1", "vite-tsconfig-paths": "5.1.4", @@ -102,4 +102,4 @@ "node": ">=22.17.0", "pnpm": ">=10.18.0" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 44eb81b2..0fd5d50c 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "clean": "git clean -fdX .", "clean:build": "git clean -fdx -e node_modules .", "typecheck": "pnpm run --filter=\"./packages/**/*\" --parallel typecheck", - "test": "pnpm run test:ci", - "test:ci": "nx run-many --targets=test:unused,check,test:deps,test:lib,test:types,test:build,build", + "test": "nx run-many --targets=test:unused,check,test:deps,test:lib,test:types,test:build,build", + "test:ci": "nx run-many --targets=test:unused,test:deps,test:lib,test:types,test:build,build", "test:cov": "pnpm run --filter=\"./packages/**/*\" --parallel test:cov", "dev": "pnpm run watch", "check": "biome check .", @@ -50,7 +50,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.29.0", - "@types/node": "22.13.1", + "@types/node": "24.10.0", "knip": "5.43.6", "lefthook": "^1.11.10", "nx": "^21.6.3", @@ -64,11 +64,11 @@ "nx": { "includedScripts": ["test:unused", "check", "test:deps"] }, - "overrides": { - "react-router-devtools": "workspace:*" - }, "pnpm": { - "onlyBuiltDependencies": ["esbuild", "msw"] + "onlyBuiltDependencies": ["esbuild", "msw"], + "overrides": { + "react-router-devtools": "workspace:*" + } }, "private": true } diff --git a/packages/react-router-devtools/README.md b/packages/react-router-devtools/README.md index 1b98596d..b100318f 100644 --- a/packages/react-router-devtools/README.md +++ b/packages/react-router-devtools/README.md @@ -62,10 +62,6 @@ If you're trying to spin it up on CF, try adding this to your `optimizeDeps` in optimizeDeps: { include: [ // other optimized deps - "beautify", - "react-diff-viewer-continued", - "classnames", - "@bkrem/react-transition-group", ], }, ``` diff --git a/packages/react-router-devtools/package.json b/packages/react-router-devtools/package.json index f4fa18e3..459bc42b 100644 --- a/packages/react-router-devtools/package.json +++ b/packages/react-router-devtools/package.json @@ -2,7 +2,7 @@ "name": "react-router-devtools", "description": "Devtools for React Router - debug, trace, find hydration errors, catch bugs and inspect server/client data with react-router-devtools", "author": "Alem Tuzlak", - "version": "5.1.6", + "version": "5.1.3", "license": "MIT", "keywords": [ "react-router", @@ -53,10 +53,6 @@ }, "types": "./dist/server.d.ts", "default": "./dist/server.js" - }, - "./client.css": { - "import": "./dist/client.css", - "require": "./dist/client.css" } }, "files": ["dist"], @@ -106,55 +102,49 @@ "vite": ">=5.0.0 || >=6.0.0" }, "devDependencies": { - "@react-router/dev": "^7.5.3", - "@react-router/node": "^7.5.3", - "@react-router/serve": "7.1.4", - "@testing-library/dom": "^10.4.0", - "@testing-library/react": "16.2.0", + "@react-router/dev": "7.9.5", + "@react-router/node": "7.9.5", + "@react-router/serve": "7.9.5", + "@tanstack/devtools": "^0.8.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "16.3.0", "@types/babel__core": "^7.20.5", "@types/babel__generator": "^7.27.0", "@types/babel__traverse": "^7.28.0", - "@types/beautify": "^0.0.3", - "@types/node": "22.13.1", + "@types/node": "24.10.0", "@vitest/coverage-v8": "3.0.5", "@vitest/ui": "3.0.5", - "autoprefixer": "^10.4.20", - "happy-dom": "16.8.1", - "jest-preview": "^0.3.1", + "happy-dom": "20.0.10", + "jest-preview": "^0.3.2", "npm-run-all": "4.1.5", - "postcss": "^8.5.1", - "tailwindcss": "^3.4.0", - "tailwindcss-animate": "^1.0.7", - "tsup": "^8.3.6", - "tsx": "4.19.2", - "typescript": "^5.8.3", - "vite": "^6.3.3", - "vite-node": "^3.1.2", + "tsup": "^8.5.0", + "tsx": "4.20.6", + "typescript": "^5.9.3", + "vite": "^7.2.2", + "vite-node": "^5.0.0", "vitest": "3.0.5" }, "dependencies": { - "@babel/core": "^7.26.10", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.10", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "@radix-ui/react-accordion": "^1.2.2", - "@radix-ui/react-select": "^2.1.5", - "beautify": "^0.0.8", - "bippy": "^0.3.7", - "chalk": "5.4.1", + "@babel/core": "^7.28.5", + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@radix-ui/react-accordion": "^1.2.12", + "@tanstack/devtools-event-client": "^0.3.4", + "@tanstack/devtools-vite": "^0.3.11", + "@tanstack/react-devtools": "^0.8.1", + "chalk": "5.6.2", "clsx": "2.1.1", - "date-fns": "^4.1.0", - "framer-motion": "^11.0.8", - "react-d3-tree": "^3.6.4", - "react-diff-viewer-continued": "^4.0.5", - "react-hotkeys-hook": "^4.6.1", - "react-tooltip": "^5.28.0", - "tailwind-merge": "3.0.1" + "framer-motion": "^12.23.24", + "goober": "^2.1.18", + "react-d3-tree": "^3.6.6", + "react-hotkeys-hook": "^5.2.1", + "react-tooltip": "^5.30.0" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "^1.9.4", - "@rollup/rollup-darwin-arm64": "^4.32.1", - "@rollup/rollup-linux-x64-gnu": "^4.32.1" + "@biomejs/cli-darwin-arm64": "^2.3.5", + "@rollup/rollup-darwin-arm64": "^4.53.2", + "@rollup/rollup-linux-x64-gnu": "^4.53.2" } } diff --git a/packages/react-router-devtools/postcss.config.js b/packages/react-router-devtools/postcss.config.js deleted file mode 100644 index 39edec5a..00000000 --- a/packages/react-router-devtools/postcss.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import autoprefixer from "autoprefixer" -import tailwindcss from "tailwindcss" -import tailwindcssNesting from "tailwindcss/nesting/index.js" -import config from "./tailwind.config.js" - -/** @type {import('postcss').Config} */ -export default { - plugins: [tailwindcssNesting(), tailwindcss(config), autoprefixer()], -} diff --git a/packages/react-router-devtools/src/client.ts b/packages/react-router-devtools/src/client.ts index 86a0fe29..b6018dcf 100644 --- a/packages/react-router-devtools/src/client.ts +++ b/packages/react-router-devtools/src/client.ts @@ -2,4 +2,10 @@ export { EmbeddedDevTools } from "./client/embedded-dev-tools.js" export { withViteDevTools } from "./client/init/root.js" export { defineClientConfig } from "./client/init/root.js" -export { withClientLoaderWrapper, withClientActionWrapper, withLinksWrapper } from "./client/hof.js" +export { + withClientLoaderWrapper, + withClientActionWrapper, + withLinksWrapper, + withClientMiddlewareWrapper, + withClientMiddlewareWrapperSingle, +} from "./client/hof.js" diff --git a/packages/react-router-devtools/src/client/components/Accordion.tsx b/packages/react-router-devtools/src/client/components/Accordion.tsx index 01ddbe48..7c2a5d8e 100644 --- a/packages/react-router-devtools/src/client/components/Accordion.tsx +++ b/packages/react-router-devtools/src/client/components/Accordion.tsx @@ -1,54 +1,47 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as React from "react" +import { cx, useStyles } from "../styles/use-styles.js" import { Icon } from "./icon/Icon.js" -import { cn } from "./util.js" const Accordion = AccordionPrimitive.Root const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) +>(({ className, ...props }, ref) => { + const { styles } = useStyles() + return +}) AccordionItem.displayName = "AccordionItem" const AccordionTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)) +>(({ className, children, ...props }, ref) => { + const { styles } = useStyles() + return ( + + + {children} + + + + ) +}) AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName const AccordionContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) +>(({ className, children, ...props }, ref) => { + const { styles } = useStyles() + return ( + +
{children}
+
+ ) +}) AccordionContent.displayName = AccordionPrimitive.Content.displayName export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/react-router-devtools/src/client/components/Breakpoints.tsx b/packages/react-router-devtools/src/client/components/Breakpoints.tsx deleted file mode 100644 index 3a3d39e0..00000000 --- a/packages/react-router-devtools/src/client/components/Breakpoints.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import clsx from "clsx" -import { useSettingsContext } from "../context/useRDTContext" -import { useOnWindowResize } from "../hooks/useOnWindowResize" - -export const Breakpoints = () => { - const { width } = useOnWindowResize() - const { settings } = useSettingsContext() - const breakpoints = settings.breakpoints - const show = settings.showBreakpointIndicator - const breakpoint = breakpoints.find((bp) => bp.min <= width && bp.max >= width) - if (!breakpoint || !breakpoint.name || !show) { - return null - } - return ( -
- {breakpoint?.name} -
- ) -} diff --git a/packages/react-router-devtools/src/client/components/CacheInfo.tsx b/packages/react-router-devtools/src/client/components/CacheInfo.tsx deleted file mode 100644 index 7eb11bb4..00000000 --- a/packages/react-router-devtools/src/client/components/CacheInfo.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { add } from "date-fns/add" -import { formatDistance } from "date-fns/formatDistance" -import type { CacheControl } from "../../server/parser.js" -import { useCountdown } from "../hooks/useCountdown.js" -import { Tag } from "./Tag.js" - -interface CacheInfoProps { - cacheDate: Date - cacheControl: CacheControl -} - -const CacheInfo = ({ cacheDate, cacheControl }: CacheInfoProps) => { - const { maxAge, sMaxage, private: isPrivate } = cacheControl - - const age = !isPrivate && !maxAge ? sMaxage : maxAge - const targetDate = add(cacheDate, { seconds: age ? Number.parseInt(age) : 0 }) - const { minutes, seconds, stringRepresentation } = useCountdown(targetDate) - const distance = formatDistance(targetDate, cacheDate, { addSuffix: true }) - if (seconds <= 0) { - return - } - return ( - - [{cacheControl.private ? "Private" : "Shared"}] Loader Cache expires {distance} ({stringRepresentation}) - - ) -} - -export { CacheInfo } diff --git a/packages/react-router-devtools/src/client/components/Checkbox.tsx b/packages/react-router-devtools/src/client/components/Checkbox.tsx deleted file mode 100644 index 3af37bc4..00000000 --- a/packages/react-router-devtools/src/client/components/Checkbox.tsx +++ /dev/null @@ -1,31 +0,0 @@ -interface CheckboxProps extends Omit, "value"> { - onChange?: (e: React.ChangeEvent) => void - id: string - children: React.ReactNode - value?: boolean - hint?: string -} - -const Checkbox = ({ onChange, id, children, value, hint, ...props }: CheckboxProps) => { - return ( -
- - {hint &&

{hint}

} -
- ) -} - -export { Checkbox } diff --git a/packages/react-router-devtools/src/client/components/EditorButton.tsx b/packages/react-router-devtools/src/client/components/EditorButton.tsx index a83a0b4e..9d4acf1d 100644 --- a/packages/react-router-devtools/src/client/components/EditorButton.tsx +++ b/packages/react-router-devtools/src/client/components/EditorButton.tsx @@ -1,21 +1,16 @@ -import clsx from "clsx" +import { useStyles } from "../styles/use-styles.js" +import { Icon } from "./icon/Icon.js" interface EditorButtonProps extends React.HTMLAttributes { onClick: () => void - name: string } -const EditorButton = ({ name, onClick, ...props }: EditorButtonProps) => { +const EditorButton = ({ onClick, ...props }: EditorButtonProps) => { + const { styles } = useStyles() return ( - ) } diff --git a/packages/react-router-devtools/src/client/components/InfoCard.tsx b/packages/react-router-devtools/src/client/components/InfoCard.tsx index 314f766b..1b74c667 100644 --- a/packages/react-router-devtools/src/client/components/InfoCard.tsx +++ b/packages/react-router-devtools/src/client/components/InfoCard.tsx @@ -1,5 +1,5 @@ -import clsx from "clsx" import type { ReactNode } from "react" +import { cx, useStyles } from "../styles/use-styles.js" export const InfoCard = ({ children, @@ -10,21 +10,13 @@ export const InfoCard = ({ title: string onClear?: () => void }) => { + const { styles } = useStyles() return ( -
-

+
+

{title} {onClear && typeof import.meta.hot === "undefined" && ( - )} diff --git a/packages/react-router-devtools/src/client/components/Input.tsx b/packages/react-router-devtools/src/client/components/Input.tsx index 12d5755a..a5cf7715 100644 --- a/packages/react-router-devtools/src/client/components/Input.tsx +++ b/packages/react-router-devtools/src/client/components/Input.tsx @@ -1,35 +1,30 @@ -import clsx from "clsx" +import { cx, useStyles } from "../styles/use-styles.js" interface InputProps extends React.InputHTMLAttributes { label?: string hint?: string } -export const Label = ({ className, children, ...props }: React.HTMLProps) => { +const Label = ({ className, children, ...props }: React.HTMLProps) => { + const { styles } = useStyles() return ( -

-
-

Route segment file: {route.id}

-
+ {/* Info Cards */} +
{loaderData && {}} - {serverInfo && import.meta.env.DEV && ( - - - - )} + {route.params && Object.keys(route.params).length > 0 && ( @@ -194,7 +123,9 @@ export const RouteSegmentInfo = ({ route, i }: { route: UIMatch )}
-
- + + ) } + +export const MemoizedRouteSegmentInfo = memo(RouteSegmentInfo) diff --git a/packages/react-router-devtools/src/client/components/RouteToggle.tsx b/packages/react-router-devtools/src/client/components/RouteToggle.tsx index bb99f760..ef0224c4 100644 --- a/packages/react-router-devtools/src/client/components/RouteToggle.tsx +++ b/packages/react-router-devtools/src/client/components/RouteToggle.tsx @@ -1,21 +1,23 @@ -import clsx from "clsx" import { useSettingsContext } from "../context/useRDTContext.js" +import { cx, useStyles } from "../styles/use-styles.js" import { Icon } from "./icon/Icon.js" export const RouteToggle = () => { + const { styles } = useStyles() const { settings, setSettings } = useSettingsContext() const { routeViewMode } = settings return ( -
+
setSettings({ routeViewMode: "tree" })} name="Network" + title="Grid" /> / setSettings({ routeViewMode: "list" })} />
diff --git a/packages/react-router-devtools/src/client/components/Select.tsx b/packages/react-router-devtools/src/client/components/Select.tsx deleted file mode 100644 index 0c4a0717..00000000 --- a/packages/react-router-devtools/src/client/components/Select.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import * as SelectPrimitive from "@radix-ui/react-select" - -import { Hint, Label } from "./Input.js" -import { Stack } from "./Stack.js" -import { Icon } from "./icon/Icon.js" -import { cn } from "./util.js" - -const Select = SelectPrimitive.Root - -const SelectGroup = SelectPrimitive.Group - -const SelectValue = SelectPrimitive.Value - -// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -const SelectTrigger = ({ className, children, ref, ...props }: SelectPrimitive.SelectTriggerProps & { ref?: any }) => ( - - {children} - - - - -) - -const SelectContent = ({ - className, - children, - position = "popper", - ref, - ...props - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -}: SelectPrimitive.SelectContentProps & { ref?: any }) => { - return ( - // @ts-ignore - - - - {children} - - - - ) -} - -// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -const SelectLabel = ({ className, ref, ...props }: SelectPrimitive.SelectLabelProps & { ref?: any }) => ( - -) - -// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -const SelectItem = ({ className, children, ref, ...props }: SelectPrimitive.SelectItemProps & { ref?: any }) => ( - - - - - - - - {children} - -) - -const SelectWithOptions = ({ - placeholder, - label, - options, - onSelect, - hint, - value, - className, -}: { - placeholder?: string - value?: T - label?: string - hint?: string - options: { value: T; label: string }[] - onSelect: (value: T) => void - className?: string -}) => { - return ( - - {label && } - - {hint && {hint}} - - ) -} - -export { SelectWithOptions } diff --git a/packages/react-router-devtools/src/client/components/Stack.tsx b/packages/react-router-devtools/src/client/components/Stack.tsx deleted file mode 100644 index 4d5a5093..00000000 --- a/packages/react-router-devtools/src/client/components/Stack.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import clsx from "clsx" - -interface StackProps extends React.HTMLAttributes { - gap?: "sm" | "md" | "lg" -} - -const GAPS = { - sm: "gap-1", - md: "gap-2", - lg: "gap-4", -} - -const Stack = ({ gap = "md", className, children, ...props }: StackProps) => { - return ( -
- {children} -
- ) -} - -export { Stack } diff --git a/packages/react-router-devtools/src/client/components/TabContent.tsx b/packages/react-router-devtools/src/client/components/TabContent.tsx new file mode 100644 index 00000000..accd18f5 --- /dev/null +++ b/packages/react-router-devtools/src/client/components/TabContent.tsx @@ -0,0 +1,10 @@ +import { useStyles } from "../styles/use-styles.js" + +interface TabContentProps { + children: React.ReactNode +} + +export const TabContent = ({ children }: TabContentProps) => { + const { styles } = useStyles() + return
{children}
+} diff --git a/packages/react-router-devtools/src/client/components/TabHeader.tsx b/packages/react-router-devtools/src/client/components/TabHeader.tsx new file mode 100644 index 00000000..318dc715 --- /dev/null +++ b/packages/react-router-devtools/src/client/components/TabHeader.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react" +import { cx, useStyles } from "../styles/use-styles.js" + +interface TabHeaderProps { + icon: ReactNode + title: string + rightContent?: ReactNode + gradientDirection?: "ltr" | "rtl" +} + +export const TabHeader = ({ icon, title, rightContent, gradientDirection = "ltr" }: TabHeaderProps) => { + const { styles } = useStyles() + return ( +
+
+ {icon} +

{title}

+
+ {rightContent &&
{rightContent}
} +
+ ) +} diff --git a/packages/react-router-devtools/src/client/components/Tag.tsx b/packages/react-router-devtools/src/client/components/Tag.tsx index b4552feb..44f4267b 100644 --- a/packages/react-router-devtools/src/client/components/Tag.tsx +++ b/packages/react-router-devtools/src/client/components/Tag.tsx @@ -1,23 +1,32 @@ -import clsx from "clsx" import type { ReactNode } from "react" +import { cx, useStyles } from "../styles/use-styles.js" export const TAG_COLORS = { - GREEN: "border-green-500 border border-solid text-white", - BLUE: "border-blue-500 border border-solid text-white", - TEAL: "border-teal-400 border border-solid text-white", - RED: "border-red-500 border border-solid text-white", - PURPLE: "border-purple-500 border border-solid text-white", + GREEN: "GREEN", + BLUE: "BLUE", + TEAL: "TEAL", + RED: "RED", + PURPLE: "PURPLE", } as const interface TagProps { color: keyof typeof TAG_COLORS children: ReactNode className?: string + size?: "small" | "default" } -const Tag = ({ color, children, className }: TagProps) => { +const Tag = ({ color, children, className, size = "default" }: TagProps) => { + const { styles } = useStyles() return ( - + ], + className + )} + > {children} ) diff --git a/packages/react-router-devtools/src/client/components/Trigger.tsx b/packages/react-router-devtools/src/client/components/Trigger.tsx deleted file mode 100644 index 1889b40c..00000000 --- a/packages/react-router-devtools/src/client/components/Trigger.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import clsx from "clsx" -import { usePersistOpen, useSettingsContext } from "../context/useRDTContext.js" -import { Logo } from "./Logo.js" - -export const Trigger = ({ - isOpen, - setIsOpen, -}: { - isOpen: boolean - setIsOpen: React.Dispatch> -}) => { - const { settings } = useSettingsContext() - const { setPersistOpen } = usePersistOpen() - const { hideUntilHover, position } = settings - const handleHover = (e: React.MouseEvent, event: "enter" | "leave") => { - if (!hideUntilHover) return - const classesToRemove = "opacity-0" - const classesToAdd = "opacity-100" - if (event === "enter") { - e.currentTarget.classList.remove(classesToRemove) - e.currentTarget.classList.add(classesToAdd) - } - if (event === "leave") { - e.currentTarget.classList.remove(classesToAdd) - e.currentTarget.classList.add(classesToRemove) - } - } - - return ( - - ) -} diff --git a/packages/react-router-devtools/src/client/components/icon/Icon.tsx b/packages/react-router-devtools/src/client/components/icon/Icon.tsx index 292c5396..2002fb86 100644 --- a/packages/react-router-devtools/src/client/components/icon/Icon.tsx +++ b/packages/react-router-devtools/src/client/components/icon/Icon.tsx @@ -1,5 +1,6 @@ import type { SVGProps } from "react" -import { cn } from "../util.js" +import { cx } from "../../styles/use-styles.js" +import { useStyles } from "../../styles/use-styles.js" import type { IconName } from "./icons/types.js" enum IconSize { @@ -16,6 +17,7 @@ type IconSizes = keyof typeof IconSize interface IconProps extends SVGProps { name: IconName + title?: string testId?: string className?: string size?: IconSizes @@ -28,12 +30,14 @@ const strokeIcon: Partial[] = [] * Icon component wrapper for SVG icons. * @returns SVG icon as a react component */ -export const Icon = ({ name, testId, className, size = "sm", ...props }: IconProps) => { +export const Icon = ({ name, title, testId, className, size = "sm", ...props }: IconProps) => { + const { styles } = useStyles() const iconSize = IconSize[size] const isEmptyFill = emptyFill.includes(name) const isStrokeIcon = strokeIcon.includes(name) - const iconClasses = cn("inline-block flex-shrink-0", className, isEmptyFill && "fill-transparent") + const iconClasses = cx(styles.icon.base, className, isEmptyFill && styles.icon.fillTransparent) return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: i don't want titles on hover - {name} @@ -13,7 +14,8 @@ const isPromise = (value: any): value is Promise => { return value && typeof value.then === "function" } -const JsonRenderer = ({ data, expansionLevel }: JsonRendererProps) => { +const JsonRendererComponent = ({ data, expansionLevel }: JsonRendererProps) => { + const { styles } = useStyles() const { settings } = useSettingsContext() const ref = useRef(true) useEffect(() => { @@ -63,7 +65,7 @@ const JsonRenderer = ({ data, expansionLevel }: JsonRendererProps) => { }, [data]) if (typeof json === "string") { - return
{json}
+ return
{json}
} return ( @@ -71,4 +73,6 @@ const JsonRenderer = ({ data, expansionLevel }: JsonRendererProps) => { ) } +const JsonRenderer = memo(JsonRendererComponent) + export { JsonRenderer } diff --git a/packages/react-router-devtools/src/client/components/Logo.tsx b/packages/react-router-devtools/src/client/components/logo.tsx similarity index 100% rename from packages/react-router-devtools/src/client/components/Logo.tsx rename to packages/react-router-devtools/src/client/components/logo.tsx diff --git a/packages/react-router-devtools/src/client/components/network-tracer/NetworkBar.tsx b/packages/react-router-devtools/src/client/components/network-tracer/NetworkBar.tsx index e8613231..d9667f67 100644 --- a/packages/react-router-devtools/src/client/components/network-tracer/NetworkBar.tsx +++ b/packages/react-router-devtools/src/client/components/network-tracer/NetworkBar.tsx @@ -1,7 +1,9 @@ -import { animate, motion, useMotionValue } from "framer-motion" +import { motion, useMotionValue } from "framer-motion" import type React from "react" import { useEffect } from "react" import type { RequestEvent } from "../../../shared/request-event" +import { cx } from "../../styles/use-styles" +import { useStyles } from "../../styles/use-styles" interface NetworkBarProps { request: RequestEvent @@ -18,8 +20,10 @@ interface NetworkBarProps { const COLORS = { loader: "#4ade80", "client-loader": "#60a5fa", - action: "#f59e0b", + action: "#FFD700", "client-action": "#ef4444", + middleware: "#FFA500", + "client-middleware": "#FF69B4", "custom-event": "#ffffff", pending: "#94a3b8", error: "#dc2626", @@ -36,75 +40,90 @@ export const NetworkBar: React.FC = ({ onClick, isActive, }) => { + const { styles } = useStyles() const startX = (request.startTime - minTime) * pixelsPerMs - const currentEndTime = request.endTime || now - const duration = currentEndTime - request.startTime const y = index * (barHeight + barPadding) + 24 const state = request.endTime ? "finished" : "pending" const color = state === "pending" ? COLORS.pending : COLORS[request.aborted ? "error" : (request.type as keyof typeof COLORS)] - const barWidth = useMotionValue(2) + // For finished requests, use the final width directly without animation + const finalWidth = request.endTime + ? Math.max(2, (request.endTime - request.startTime) * pixelsPerMs) + : Math.max(2, (now - request.startTime) * pixelsPerMs) + + const barWidth = useMotionValue(finalWidth) useEffect(() => { - const updateWidth = () => { - if (request.endTime) { - animate(barWidth, Math.max(2, (request.endTime - request.startTime) * pixelsPerMs), { - duration: 0.3, - ease: "easeOut", - }) - } else if (isActive) { - barWidth.set(Math.max(2, (now - request.startTime) * pixelsPerMs)) - requestAnimationFrame(updateWidth) + // Only animate if the request is not finished + if (!request.endTime && isActive) { + let animationFrameId: number + + const updateWidth = () => { + const currentWidth = Math.max(2, (Date.now() - request.startTime) * pixelsPerMs) + barWidth.set(currentWidth) + animationFrameId = requestAnimationFrame(updateWidth) } - } - if (isActive) { - requestAnimationFrame(updateWidth) - } + animationFrameId = requestAnimationFrame(updateWidth) - if (!isActive) { - barWidth.stop() + return () => { + cancelAnimationFrame(animationFrameId) + barWidth.stop() + } } - return () => { - barWidth.stop() + // For finished requests, set the width once and never change it + if (request.endTime) { + barWidth.set(finalWidth) } - }, [request.endTime, request.startTime, pixelsPerMs, now, barWidth, isActive]) + }, [request.endTime, request.startTime, pixelsPerMs, barWidth, isActive, finalWidth]) + + const currentEndTime = request.endTime || now + const duration = currentEndTime - request.startTime return ( onClick(e, request, index)} > - {isActive && ( + {!request.endTime && ( )} -
- {request.method} {request.url} +
+ {request.id} - {request.method} {request.url}
{request.endTime ? `Duration: ${duration.toFixed(0)}ms` : `Elapsed: ${duration.toFixed(0)}ms...`}
diff --git a/packages/react-router-devtools/src/client/components/network-tracer/NetworkPanel.tsx b/packages/react-router-devtools/src/client/components/network-tracer/NetworkPanel.tsx index 97ddc6b6..338d6c3d 100644 --- a/packages/react-router-devtools/src/client/components/network-tracer/NetworkPanel.tsx +++ b/packages/react-router-devtools/src/client/components/network-tracer/NetworkPanel.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from "react" import { useRequestContext } from "../../context/requests/request-context" +import { cx } from "../../styles/use-styles" +import { useStyles } from "../../styles/use-styles" import NetworkWaterfall from "./NetworkWaterfall" function NetworkPanel() { const { requests } = useRequestContext() - + const { styles } = useStyles() const [containerWidth, setContainerWidth] = useState(800) // Simulate network requests for demo @@ -26,10 +28,10 @@ function NetworkPanel() { }, []) return ( -
-
-
-
+
+
+
+
diff --git a/packages/react-router-devtools/src/client/components/network-tracer/NetworkWaterfall.tsx b/packages/react-router-devtools/src/client/components/network-tracer/NetworkWaterfall.tsx index 3d0ed418..3cd5b071 100644 --- a/packages/react-router-devtools/src/client/components/network-tracer/NetworkWaterfall.tsx +++ b/packages/react-router-devtools/src/client/components/network-tracer/NetworkWaterfall.tsx @@ -4,51 +4,98 @@ import { useEffect, useRef, useState } from "react" import { useHotkeys } from "react-hotkeys-hook" import { Tooltip } from "react-tooltip" import type { RequestEvent } from "../../../shared/request-event" +import { cx } from "../../styles/use-styles" +import { useStyles } from "../../styles/use-styles" import { METHOD_COLORS } from "../../tabs/TimelineTab" import { Tag } from "../Tag" import { NetworkBar } from "./NetworkBar" -import { REQUEST_BORDER_COLORS, RequestDetails } from "./RequestDetails" +import { RequestDetails } from "./RequestDetails" interface Props { requests: RequestEvent[] width: number } -const BAR_HEIGHT = 20 -const BAR_PADDING = 8 +const BAR_HEIGHT = 16 +const BAR_PADDING = 6 const TIME_COLUMN_INTERVAL = 1000 // 1 second const _MIN_SCALE = 0.1 const _MAX_SCALE = 10 const FUTURE_BUFFER = 1000 // 2 seconds ahead const INACTIVE_THRESHOLD = 100 // 1 seconds -const TYPE_COLORS = { - loader: "bg-green-500", - "client-loader": "bg-blue-500", - action: "bg-yellow-500", - "client-action": "bg-purple-500", - "custom-event": "bg-white", -} const TYPE_TEXT_COLORS = { loader: "text-green-500", "client-loader": "text-blue-500", action: "text-yellow-500", "client-action": "text-purple-500", + middleware: "text-orange-500", + "client-middleware": "text-pink-400", "custom-event": "text-white", } +type EventType = + | "loader" + | "client-loader" + | "action" + | "client-action" + | "middleware" + | "client-middleware" + | "custom-event" + +const EVENT_TYPE_FILTERS: { value: EventType | "all"; label: string; color: string }[] = [ + { value: "all", label: "All Events", color: "#ffffff" }, + { value: "loader", label: "Loader", color: "#4ade80" }, + { value: "client-loader", label: "Client Loader", color: "#60a5fa" }, + { value: "action", label: "Action", color: "#FFD700" }, + { value: "client-action", label: "Client Action", color: "#ef4444" }, + { value: "middleware", label: "Middleware", color: "#FFA500" }, + { value: "client-middleware", label: "Client Middleware", color: "#FF69B4" }, + { value: "custom-event", label: "Custom Event", color: "#ffffff" }, +] + const NetworkWaterfall: React.FC = ({ requests, width }) => { const containerRef = useRef(null) + const { styles } = useStyles() const [scale, _setScale] = useState(0.1) const [isDragging, setIsDragging] = useState(false) const [dragStart, setDragStart] = useState({ x: 0, scrollLeft: 0 }) const [selectedRequestIndex, setSelectedRequest] = useState(null) const [now, setNow] = useState(Date.now()) - const selectedRequest = selectedRequestIndex !== null ? requests[selectedRequestIndex] : null + const [activeTypeFilter, setActiveTypeFilter] = useState("all") + const [activeRouteFilters, setActiveRouteFilters] = useState>(new Set()) + + // Get unique routes from all requests + const uniqueRoutes = Array.from(new Set(requests.map((req) => req.routeId))).sort() + + // Filter requests based on active filters + let filteredRequests = requests + + // Apply type filter + if (activeTypeFilter !== "all") { + filteredRequests = filteredRequests.filter((req) => req.type === activeTypeFilter) + } + + // Apply route filters (if any selected) + if (activeRouteFilters.size > 0) { + filteredRequests = filteredRequests.filter((req) => activeRouteFilters.has(req.routeId)) + } + + // Get the selected request from the filtered list + const selectedRequest = selectedRequestIndex !== null ? filteredRequests[selectedRequestIndex] : null + // Check if there are any active requests - const hasActiveRequests = requests.some( + const hasActiveRequests = filteredRequests.some( (req) => !req.endTime || (req.endTime && now - req.endTime < INACTIVE_THRESHOLD) ) + + // Reset selected index when filters change and current selection is out of bounds + useEffect(() => { + if (selectedRequestIndex !== null && selectedRequestIndex >= filteredRequests.length) { + setSelectedRequest(null) + } + }, [selectedRequestIndex, filteredRequests.length]) + useEffect(() => { if (!hasActiveRequests) { return @@ -57,11 +104,11 @@ const NetworkWaterfall: React.FC = ({ requests, width }) => { return () => clearInterval(interval) }, [hasActiveRequests]) - const minTime = Math.min(...requests.map((r) => r.startTime)) + const minTime = Math.min(...filteredRequests.map((r) => r.startTime)) const maxTime = hasActiveRequests ? now + FUTURE_BUFFER - : requests.length > 0 - ? Math.max(...requests.map((r) => r.endTime || r.startTime)) + 1000 + : filteredRequests.length > 0 + ? Math.max(...filteredRequests.map((r) => r.endTime || r.startTime)) + 1000 : now const duration = maxTime - minTime const pixelsPerMs = scale @@ -134,84 +181,233 @@ const NetworkWaterfall: React.FC = ({ requests, width }) => { if (e.key === "ArrowLeft" && order > 0) { onChangeRequest(order - 1) } - if (e.key === "ArrowRight" && order < requests.length - 1) { + if (e.key === "ArrowRight" && order < filteredRequests.length - 1) { onChangeRequest(order + 1) } }) + + const toggleRouteFilter = (route: string) => { + setActiveRouteFilters((prev) => { + const newFilters = new Set(prev) + if (newFilters.has(route)) { + newFilters.delete(route) + } else { + newFilters.add(route) + } + return newFilters + }) + } + + const clearRouteFilters = () => { + setActiveRouteFilters(new Set()) + } + return ( -
-
-
-
Requests
-
- {requests.map((request, index) => ( -
+ {/* Type Filter Bar */} +
+
Type:
+
+ {EVENT_TYPE_FILTERS.map((filter) => ( + + ))} +
+
+ + {/* Route Filter Bar */} +
+
+ Routes: + {activeRouteFilters.size > 0 && ( + + ({activeRouteFilters.size} selected) + + )} +
+
+ + {uniqueRoutes.map((route) => { + const isActive = activeRouteFilters.has(route) + const routeCount = requests.filter((r) => r.routeId === route).length + return ( + + ) + })} +
+
+ + {/* Results summary */} + {(activeTypeFilter !== "all" || activeRouteFilters.size > 0) && ( +
+ Showing {filteredRequests.length} of {requests.length} events + {activeRouteFilters.size > 0 && ( + + )} +
+ )} + +
+
+
Requests
+
+ {filteredRequests.map((request, index) => { + const borderColorClass = + request.type === "loader" + ? styles.network.waterfall.requestButtonGreen + : request.type === "client-loader" + ? styles.network.waterfall.requestButtonBlue + : request.type === "action" + ? styles.network.waterfall.requestButtonYellow + : request.type === "client-action" + ? styles.network.waterfall.requestButtonPurple + : request.type === "middleware" + ? styles.network.waterfall.requestButtonOrange + : request.type === "client-middleware" + ? styles.network.waterfall.requestButtonPinkLight + : styles.network.waterfall.requestButtonWhite + + const indicatorColorClass = + request.type === "loader" + ? styles.network.waterfall.requestIndicatorGreen + : request.type === "client-loader" + ? styles.network.waterfall.requestIndicatorBlue + : request.type === "action" + ? styles.network.waterfall.requestIndicatorYellow + : request.type === "client-action" + ? styles.network.waterfall.requestIndicatorPurple + : request.type === "middleware" + ? styles.network.waterfall.requestIndicatorOrange + : request.type === "client-middleware" + ? styles.network.waterfall.requestIndicatorPinkLight + : styles.network.waterfall.requestIndicatorWhite + + return ( +
-
This was triggered by ${request.type.startsWith("a") ? "an" : "a"} ${request.type} request
`} - data-tooltip-place="top" - className={`size-2 p-1 ${TYPE_COLORS[request.type]}`} - /> + +
+ {request?.method && ( + + {request.method} + + )}
- -
- {request?.method && ( - - {request.method} - - )}
-
- ))} + ) + })}
-
-
+
+
{Array.from({ length: timeColumns }).map((_, i) => (
key={i} - className="absolute top-0 h-full border-r-none border-t-none border-b-none !border-l border-white border-l-2 text-sm text-white " + className={styles.network.waterfall.timeColumn} style={{ left: i * TIME_COLUMN_INTERVAL * pixelsPerMs, }} > - {i}s + {i}s
))}
- - {requests.map((request, index) => ( + + {filteredRequests.map((request, index) => ( = ({ requests, width }) => {
-
- {selectedRequest && ( - - - - )} -
- {/*
- - - -
Scale: {scale.toFixed(2)}x
-
*/} + {selectedRequest && ( + + + + )}
) } diff --git a/packages/react-router-devtools/src/client/components/network-tracer/RequestDetails.tsx b/packages/react-router-devtools/src/client/components/network-tracer/RequestDetails.tsx index 57aa4e7b..18aff7ba 100644 --- a/packages/react-router-devtools/src/client/components/network-tracer/RequestDetails.tsx +++ b/packages/react-router-devtools/src/client/components/network-tracer/RequestDetails.tsx @@ -1,5 +1,7 @@ import type React from "react" import type { RequestEvent } from "../../../shared/request-event" +import { cx } from "../../styles/use-styles" +import { useStyles } from "../../styles/use-styles" import { METHOD_COLORS } from "../../tabs/TimelineTab" import { Tag } from "../Tag" import { Icon } from "../icon/Icon" @@ -12,97 +14,155 @@ interface RequestDetailsProps { total: number index: null | number } -export const REQUEST_BORDER_COLORS = { - loader: "border-green-500", - "client-loader": "border-blue-500", - action: "border-yellow-500", - "client-action": "border-purple-500", - "custom-event": "border-white", - error: "border-red-500", -} + export const RequestDetails: React.FC = ({ request, onClose, total, index, onChangeRequest }) => { + const { styles } = useStyles() + if (index === null) { return } + + const typeBadgeColorClass = + request.type === "loader" + ? styles.network.details.typeBadgeGreen + : request.type === "client-loader" + ? styles.network.details.typeBadgeBlue + : request.type === "action" + ? styles.network.details.typeBadgeYellow + : request.type === "client-action" + ? styles.network.details.typeBadgePurple + : request.type === "middleware" + ? styles.network.details.typeBadgeOrange + : request.type === "client-middleware" + ? styles.network.details.typeBadgePinkLight + : styles.network.details.typeBadgeWhite + + const duration = request.endTime ? request.endTime - request.startTime : 0 + return ( -
-
-
-
-
- {request?.method && ( - - {request.method} - - )} - {request?.type && ( -
- {request.type} -
- )} - {request?.aborted && ( -
- Request aborted -
- )} -
-
-
+
+
+ {/* Header with close button */} +
+
+
Request Details
+
+
{index > 0 ? ( ) : null} {index < total - 1 ? ( ) : null}
-
- {request.id} - {request.url} + + {/* Main request info */} +
+
+
{request.url}
+
ID: {request.id}
+
+
-
- Request duration: {new Date(request.startTime).toLocaleTimeString()}{" "} - {request.endTime && `- ${new Date(request.endTime).toLocaleTimeString()} `} + {/* Metadata grid */} +
+
+
Method
+
+ {request?.method && ( + + {request.method} + + )} +
+
+ +
+
Type
+
+ {request?.type && ( +
{request.type}
+ )} +
+
+ +
+
Duration
+
+ {request.endTime ? ( + {duration.toFixed(0)}ms + ) : ( + Pending... + )} +
+
+ +
+
Started
+
+ {new Date(request.startTime).toLocaleTimeString()} +
+
+ {request.endTime && ( - ({(request.endTime - request.startTime).toFixed(0)}ms) +
+
Completed
+
+ {new Date(request.endTime).toLocaleTimeString()} +
+
+ )} + + {request?.aborted && ( +
+
Status
+
+
Aborted
+
+
)}
+ {/* Data sections */} {request.data && ( -
-
Returned Data
-
+
+
+ + Response Data +
+
)} + {request.headers && Object.keys(request.headers).length > 0 && ( -
-
Headers
-
+
+
+ + Headers +
+
diff --git a/packages/react-router-devtools/src/client/components/util.ts b/packages/react-router-devtools/src/client/components/util.ts deleted file mode 100644 index 5e1abdf4..00000000 --- a/packages/react-router-devtools/src/client/components/util.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/packages/react-router-devtools/src/client/context/RDTContext.test.tsx b/packages/react-router-devtools/src/client/context/RDTContext.test.tsx index 009f97b2..ddaffbeb 100644 --- a/packages/react-router-devtools/src/client/context/RDTContext.test.tsx +++ b/packages/react-router-devtools/src/client/context/RDTContext.test.tsx @@ -1,19 +1,6 @@ import { render } from "@testing-library/react" -import * as detachedMethods from "../utils/detached.js" -import { - REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, - REACT_ROUTER_DEV_TOOLS_DETACHED, - REACT_ROUTER_DEV_TOOLS_SETTINGS, - REACT_ROUTER_DEV_TOOLS_STATE, -} from "../utils/storage.js" -import { - RDTContextProvider, - //detachedModeSetup, - getSettings, - resetIsDetachedCheck, - setIsDetachedIfRequired, - //getExistingStateFromStorage, -} from "./RDTContext.js" +import { REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE } from "../utils/storage.js" +import { RDTContextProvider, getSettings } from "./RDTContext.js" vi.mock("react-router", () => ({ useLocation: () => ({ @@ -85,51 +72,3 @@ describe("getSettings", () => { expect(settings).toEqual(storedSettings) }) }) - -describe("setIsDetachedIfRequired", () => { - it('should set REACT_ROUTER_DEV_TOOLS_DETACHED to "true" if window is not detached but RDT_MOUNTED is true', () => { - const isDetachedWindowSpy = vi.spyOn(detachedMethods, "checkIsDetachedWindow").mockReturnValue(false) - const setSessionSpy = vi.spyOn(sessionStorage, "setItem") - const window = { RDT_MOUNTED: true } - - // biome-ignore lint/suspicious/noExplicitAny: test - ;(global as any).window = window - setIsDetachedIfRequired() - expect(isDetachedWindowSpy).toHaveBeenCalled() - expect(setSessionSpy).toHaveBeenCalledWith(REACT_ROUTER_DEV_TOOLS_DETACHED, "true") - }) - - it("should not set REACT_ROUTER_DEV_TOOLS_DETACHED if window is detached", () => { - const isDetachedWindowSpy = vi.spyOn(detachedMethods, "checkIsDetachedWindow").mockReturnValue(true) - const setSessionSpy = vi.spyOn(sessionStorage, "setItem") - const window = { RDT_MOUNTED: false } - - // biome-ignore lint/suspicious/noExplicitAny: test - ;(global as any).window = window - - setIsDetachedIfRequired() - expect(isDetachedWindowSpy).toHaveBeenCalled() - expect(setSessionSpy).not.toHaveBeenCalled() - }) - - it("should not set REACT_ROUTER_DEV_TOOLS_DETACHED if RDT_MOUNTED is false && isDetachedWindow is false", () => { - const isDetachedWindowSpy = vi.spyOn(detachedMethods, "checkIsDetachedWindow").mockReturnValue(false) - const setSessionSpy = vi.spyOn(sessionStorage, "setItem") - const window = { RDT_MOUNTED: false } - // biome-ignore lint/suspicious/noExplicitAny: test - ;(global as any).window = window - - setIsDetachedIfRequired() - expect(isDetachedWindowSpy).toHaveBeenCalled() - expect(setSessionSpy).not.toHaveBeenCalled() - }) -}) - -describe("resetIsDetachedCheck", () => { - it('should set REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED to "false" whenever the window is mounted', () => { - const setStorageSpy = vi.spyOn(localStorage, "setItem") - - resetIsDetachedCheck() - expect(setStorageSpy).toHaveBeenCalledWith(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, "false") - }) -}) diff --git a/packages/react-router-devtools/src/client/context/RDTContext.tsx b/packages/react-router-devtools/src/client/context/RDTContext.tsx index 4329c392..856df049 100644 --- a/packages/react-router-devtools/src/client/context/RDTContext.tsx +++ b/packages/react-router-devtools/src/client/context/RDTContext.tsx @@ -2,16 +2,11 @@ import type { Dispatch } from "react" import type React from "react" import { createContext, useEffect, useMemo, useReducer } from "react" import { bigIntReplacer } from "../../shared/bigint-util.js" -import { useRemoveBody } from "../hooks/detached/useRemoveBody.js" -import { checkIsDetached, checkIsDetachedOwner, checkIsDetachedWindow } from "../utils/detached.js" import { tryParseJson } from "../utils/sanitize.js" import { - REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, - REACT_ROUTER_DEV_TOOLS_DETACHED, REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE, getStorageItem, - setSessionItem, setStorageItem, } from "../utils/storage.js" import { @@ -33,37 +28,6 @@ interface ContextProps { config?: RdtClientConfig } -export const setIsDetachedIfRequired = () => { - const isDetachedWindow = checkIsDetachedWindow() - if (!isDetachedWindow && window.RDT_MOUNTED) { - setSessionItem(REACT_ROUTER_DEV_TOOLS_DETACHED, "true") - } -} - -export const resetIsDetachedCheck = () => { - setStorageItem(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, "false") -} - -export const detachedModeSetup = () => { - resetIsDetachedCheck() - setIsDetachedIfRequired() - const isDetachedWindow = checkIsDetachedWindow() - const isDetached = checkIsDetached() - const isDetachedOwner = checkIsDetachedOwner() - - if (isDetachedWindow && !isDetached) { - window.close() - } - if (!isDetached && isDetachedOwner) { - //setSessionItem(REMIX_DEV_TOOLS_DETACHED_OWNER, "false"); - // isDetachedOwner = false; - } - return { - detachedWindow: window.RDT_MOUNTED ?? isDetachedWindow, - detachedWindowOwner: isDetachedOwner, - } -} - export const getSettings = () => { const settingsString = getStorageItem(REACT_ROUTER_DEV_TOOLS_SETTINGS) const settings = tryParseJson(settingsString) @@ -72,11 +36,10 @@ export const getSettings = () => { } } -export const getExistingStateFromStorage = (config?: RdtClientConfig & { editorName?: string }) => { +const getExistingStateFromStorage = (config?: RdtClientConfig & { editorName?: string }) => { const existingState = getStorageItem(REACT_ROUTER_DEV_TOOLS_STATE) const settings = getSettings() - const { detachedWindow, detachedWindowOwner } = detachedModeSetup() const state: ReactRouterDevtoolsState = { ...initialState, ...(existingState ? JSON.parse(existingState) : {}), @@ -85,45 +48,20 @@ export const getExistingStateFromStorage = (config?: RdtClientConfig & { editorN ...config, ...settings, editorName: config?.editorName ?? initialState.settings.editorName, - liveUrls: config?.liveUrls ?? initialState.settings.liveUrls, - breakpoints: config?.breakpoints ?? initialState.settings.breakpoints, }, - detachedWindow, - detachedWindowOwner, } return state } -export type RdtClientConfig = Pick< - ReactRouterDevtoolsState["settings"], - | "defaultOpen" - | "breakpoints" - | "showBreakpointIndicator" - | "showRouteBoundariesOn" - | "expansionLevel" - | "liveUrls" - | "position" - | "height" - | "minHeight" - | "maxHeight" - | "hideUntilHover" - | "panelLocation" - | "requireUrlFlag" - | "openHotkey" - | "urlFlag" - | "enableInspector" - | "routeBoundaryGradient" -> +export type RdtClientConfig = Pick export const RDTContextProvider = ({ children, config }: ContextProps) => { const [state, dispatch] = useReducer(rdtReducer, getExistingStateFromStorage(config)) // biome-ignore lint/correctness/useExhaustiveDependencies: investigate const value = useMemo(() => ({ state, dispatch }), [state, dispatch]) - useRemoveBody(state) - useEffect(() => { - const { settings, detachedWindow, detachedWindowOwner, ...rest } = state + const { settings, ...rest } = state // Store user settings for dev tools into local storage setStorageItem(REACT_ROUTER_DEV_TOOLS_SETTINGS, JSON.stringify(settings)) // Store general state into local storage diff --git a/packages/react-router-devtools/src/client/context/rdtReducer.test.ts b/packages/react-router-devtools/src/client/context/rdtReducer.test.ts index 6c8db435..db6c975d 100644 --- a/packages/react-router-devtools/src/client/context/rdtReducer.test.ts +++ b/packages/react-router-devtools/src/client/context/rdtReducer.test.ts @@ -1,5 +1,6 @@ import { initialState, rdtReducer } from "./rdtReducer.js" import type { TimelineEvent } from "./timeline/types.js" + const timelineEvent: TimelineEvent = { to: "background", type: "REDIRECT", @@ -9,8 +10,6 @@ const timelineEvent: TimelineEvent = { id: "id", } -const terminal = initialState.terminals[0] - describe("rdtReducer", () => { it("should return the initial state", () => { // biome-ignore lint/suspicious/noExplicitAny: test @@ -22,7 +21,6 @@ describe("rdtReducer", () => { routeWildcards: { "/foo": { wildcard: "bar" }, }, - shouldConnectWithForge: true, } const expectedState = { ...initialState, @@ -76,67 +74,6 @@ describe("rdtReducer", () => { ).toEqual(expectedState) }) - it("should handle SET_PROCESS_ID", () => { - const expectedState = { - ...initialState, - terminals: [{ ...terminal, processId: 1 }], - } - expect( - rdtReducer(initialState, { - type: "SET_PROCESS_ID", - payload: { terminalId: 0, processId: 1 }, - }) - ).toEqual(expectedState) - }) - - it("should not set process id if terminal doesn't exist", () => { - const expectedState = { - ...initialState, - terminals: [terminal], - } - expect( - rdtReducer(initialState, { - type: "SET_PROCESS_ID", - payload: { terminalId: 5, processId: 1 }, - }) - ).toEqual(expectedState) - }) - - it("should handle TOGGLE_TERMINAL_LOCK", () => { - const expectedState = { - ...initialState, - terminals: [{ ...terminal, locked: true }], - } - expect( - rdtReducer(initialState, { - type: "TOGGLE_TERMINAL_LOCK", - payload: { terminalId: 0, locked: true }, - }) - ).toEqual(expectedState) - }) - - it("should handle TOGGLE_TERMINAL_LOCK and change the lock on the correct terminal", () => { - const expectedState = { - ...initialState, - terminals: [ - { ...terminal, locked: true }, - { ...terminal, id: 1 }, - ], - } - expect( - rdtReducer( - { - ...initialState, - terminals: [...initialState.terminals, { ...terminal, id: 1 }], - }, - { - type: "TOGGLE_TERMINAL_LOCK", - payload: { terminalId: 0, locked: true }, - } - ) - ).toEqual(expectedState) - }) - it("should handle SET_PERSIST_OPEN", () => { const expectedState = { ...initialState, @@ -150,164 +87,20 @@ describe("rdtReducer", () => { ).toEqual(expectedState) }) - it("should handle SET_IS_SUBMITTED", () => { - const expectedState = { - ...initialState, - isSubmitted: true, - } - expect( - rdtReducer(initialState, { - type: "SET_IS_SUBMITTED", - payload: undefined, - }) - ).toEqual(expectedState) - }) - - it("should remove terminal if found", () => { - const expectedState = { - ...initialState, - terminals: [], - } - expect( - rdtReducer(initialState, { - type: "ADD_OR_REMOVE_TERMINAL", - payload: 0, - }) - ).toEqual(expectedState) - }) - - it("should add terminal if not found", () => { - const expectedState = { - ...initialState, - terminals: [{ ...terminal }, { ...terminal, id: 1 }], - } - - expect( - rdtReducer(initialState, { - type: "ADD_OR_REMOVE_TERMINAL", - payload: 1, - }) - ).toEqual(expectedState) - }) - - it("should add terminal output", () => { - const expectedState = { - ...initialState, - terminals: [ - { - ...terminal, - output: [{ type: "command", value: "test" }], - }, - ], - } - expect( - rdtReducer(initialState, { - type: "ADD_TERMINAL_OUTPUT", - payload: { terminalId: 0, output: { type: "command", value: "test" } }, - }) - ).toEqual(expectedState) - }) - - it("should not add terminal output if terminal does not exist", () => { - const expectedState = { - ...initialState, - } - expect( - rdtReducer(initialState, { - type: "ADD_TERMINAL_OUTPUT", - payload: { terminalId: 2, output: { type: "command", value: "test" } }, - }) - ).toEqual(expectedState) - }) - - it("should clear all terminal output for a valid terminalId", () => { - const expectedState = { - ...initialState, - terminals: [ - { - ...terminal, - output: [], - }, - ], - } - expect( - rdtReducer( - { - ...initialState, - terminals: [ - { - ...terminal, - output: [{ type: "command", value: "test" }], - }, - ], - }, - { - type: "CLEAR_TERMINAL_OUTPUT", - payload: 0, - } - ) - ).toEqual(expectedState) - }) - - it("should not clear any output if terminal doesn't exist", () => { - const expectedState = { - ...initialState, - terminals: [{ ...terminal, output: [{ type: "command", value: "test" }] }], - } - expect( - rdtReducer( - { - ...initialState, - terminals: [{ ...terminal, output: [{ type: "command", value: "test" }] }], - }, - { - type: "CLEAR_TERMINAL_OUTPUT", - payload: 2, - } - ) - ).toEqual(expectedState) - }) - - it("should add terminal history for a given terminal", () => { - const expectedState = { + it("should handle SET_WHOLE_STATE", () => { + const newState = { ...initialState, - terminals: [ - { - ...terminal, - history: ["npm run version"], - }, - ], - } - expect( - rdtReducer(initialState, { - type: "ADD_TERMINAL_HISTORY", - payload: { terminalId: 0, history: "npm run version" }, - }) - ).toEqual(expectedState) - }) - - it("should not add terminal history for a terminal that doesn't exist", () => { - const expectedState = { - ...initialState, - } - expect( - rdtReducer(initialState, { - type: "ADD_TERMINAL_HISTORY", - payload: { terminalId: 2, history: "npm run version" }, - }) - ).toEqual(expectedState) - }) - - it("should toggle lock value when payload value is not specified", () => { - const expectedState = { - ...initialState, - terminals: [{ ...terminal, locked: true }], + persistOpen: true, + settings: { + ...initialState.settings, + activeTab: "routes" as const, + }, } expect( rdtReducer(initialState, { - type: "TOGGLE_TERMINAL_LOCK", - payload: { terminalId: 0 }, + type: "SET_WHOLE_STATE", + payload: newState, }) - ).toEqual(expectedState) + ).toEqual(newState) }) }) diff --git a/packages/react-router-devtools/src/client/context/rdtReducer.ts b/packages/react-router-devtools/src/client/context/rdtReducer.ts index 98dcc1eb..8b2ac18f 100644 --- a/packages/react-router-devtools/src/client/context/rdtReducer.ts +++ b/packages/react-router-devtools/src/client/context/rdtReducer.ts @@ -1,90 +1,23 @@ -import type { ActionEvent, LoaderEvent } from "../../server/event-queue.js" import type { Tabs } from "../tabs/index.js" import { cutArrayToFirstN } from "../utils/common.js" -import type { Terminal } from "./terminal/types.js" import type { TimelineEvent } from "./timeline/types.js" -export const defaultServerRouteState: ServerRouteInfo = { - highestExecutionTime: 0, - lowestExecutionTime: 0, - averageExecutionTime: 0, - loaderTriggerCount: 0, - actionTriggerCount: 0, - lastAction: {}, - lastLoader: {}, - loaders: [], - actions: [], -} -// classes created in input.css +// Gradient keys for use with Goober styles export const ROUTE_BOUNDARY_GRADIENTS = { - sea: "sea-gradient", - hyper: "hyper-gradient", - gotham: "gotham-gradient", - gray: "gray-gradient", - watermelon: "watermelon-gradient", - ice: "ice-gradient", - silver: "silver-gradient", + sea: "sea", + hyper: "hyper", + gotham: "gotham", + gray: "gray", + watermelon: "watermelon", + ice: "ice", + silver: "silver", } as const -export const RouteBoundaryOptions = Object.keys(ROUTE_BOUNDARY_GRADIENTS) as (keyof typeof ROUTE_BOUNDARY_GRADIENTS)[] export type RouteWildcards = Record | undefined> -type TriggerPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right" | "middle-left" | "middle-right" - -export type ServerRouteInfo = { - actions?: Omit[] - loaders?: Omit[] - lowestExecutionTime: number - highestExecutionTime: number - averageExecutionTime: number - loaderTriggerCount: number - actionTriggerCount: number - lastAction: Partial> - lastLoader: Partial> -} - -export type ServerInfo = { - port?: number - routes?: { - [key: string]: ServerRouteInfo - } -} - -type HTMLErrorPrimitive = { - file?: string - tag: string -} - -export type HTMLError = { - child: HTMLErrorPrimitive - parent: HTMLErrorPrimitive -} export type ReactRouterDevtoolsState = { timeline: TimelineEvent[] - terminals: Terminal[] settings: { - /** - * Enables the bippy inspector to inspect react components - */ - enableInspector: boolean - /** - * The breakpoints to show in the corner so you can see the current breakpoint that you defined - */ - breakpoints: { name: string; min: number; max: number }[] - /** - * Whether to show the breakpoint indicator - */ - showBreakpointIndicator: boolean - /** - * The live urls to show in the corner which allow you to open the app in a different environment (eg. staging, production) - * @default [] - */ - liveUrls: { url: string; name: string }[] - /** - * The position of the live urls - * @default "bottom-left" - */ - liveUrlsPosition: "bottom-left" | "bottom-right" | "top-left" | "top-right" /** * The route boundary gradient color to use * @default "silver" @@ -97,32 +30,6 @@ export type ReactRouterDevtoolsState = { routeBoundaryGradient: keyof typeof ROUTE_BOUNDARY_GRADIENTS routeWildcards: RouteWildcards activeTab: Tabs - height: number - /** - * The maximum height of the panel - * @default 800 - */ - maxHeight: number - /** - * The minimum height of the panel - * @default 200 - */ - minHeight: number - /** - * Whether the dev tools should be open by default - * @default false - */ - defaultOpen: boolean - /** - * Whether the dev tools trigger should be hidden until the user hovers over it - * @default false - */ - hideUntilHover: boolean - /** - * The position of the trigger button - * @default "bottom-right" - */ - position: TriggerPosition /** * The initial expansion level of the JSON viewer objects * @default 1 @@ -131,81 +38,26 @@ export type ReactRouterDevtoolsState = { hoveredRoute: string isHoveringRoute: boolean routeViewMode: "list" | "tree" - /** - * The location of the panel once it is open - * @default "bottom" - */ - panelLocation: "top" | "bottom" + withServerDevTools: boolean - /** - * The hotkey to open the dev tools - * @default "shift+a" - */ - openHotkey: string - /** - * Whether to require the URL flag to open the dev tools - * @default false - */ - requireUrlFlag: boolean - /** - * The URL flag to open the dev tools, used in conjunction with requireUrlFlag (if set to true) - * @default "rdt" - */ - urlFlag: string - /** - * Whether to show route boundaries on hover of the route segment or clicking a button - */ - showRouteBoundariesOn: "hover" | "click" } - htmlErrors: HTMLError[] - server?: ServerInfo persistOpen: boolean - detachedWindow: boolean - detachedWindowOwner: boolean } export const initialState: ReactRouterDevtoolsState = { timeline: [], - terminals: [{ id: 0, locked: false, output: [], history: [] }], - server: undefined, settings: { - enableInspector: false, - showRouteBoundariesOn: "click", - breakpoints: [ - { name: "", min: 0, max: 639 }, - { name: "sm", min: 640, max: 767 }, - { name: "md", min: 768, max: 1023 }, - { name: "lg", min: 1024, max: 1279 }, - { name: "xl", min: 1280, max: 1535 }, - { name: "2xl", min: 1536, max: 9999 }, - ], - showBreakpointIndicator: true, - liveUrls: [], - liveUrlsPosition: "bottom-left", editorName: "VSCode", routeBoundaryGradient: "watermelon", routeWildcards: {}, activeTab: "page", - height: 400, - maxHeight: 600, - minHeight: 200, - defaultOpen: false, - hideUntilHover: false, - position: "bottom-right", expansionLevel: 1, hoveredRoute: "", isHoveringRoute: false, routeViewMode: "tree", - panelLocation: "bottom", withServerDevTools: true, - openHotkey: "shift+a", - requireUrlFlag: false, - urlFlag: "rdt", }, - htmlErrors: [], persistOpen: false, - detachedWindow: false, - detachedWindowOwner: false, } /** Reducer action types */ @@ -214,53 +66,6 @@ type SetTimelineEvent = { payload: TimelineEvent } -type ToggleTerminalLock = { - type: "TOGGLE_TERMINAL_LOCK" - payload: { - terminalId: Terminal["id"] - locked?: boolean - } -} - -type AddOrRemoveTerminal = { - type: "ADD_OR_REMOVE_TERMINAL" - payload?: Terminal["id"] -} - -type AddTerminalOutput = { - type: "ADD_TERMINAL_OUTPUT" - payload: { - terminalId: Terminal["id"] - output: Terminal["output"][number] - } -} - -type AddTerminalHistory = { - type: "ADD_TERMINAL_HISTORY" - payload: { - terminalId: Terminal["id"] - history: Terminal["history"][number] - } -} - -type ClearTerminalOutput = { - type: "CLEAR_TERMINAL_OUTPUT" - payload: Terminal["id"] -} - -type SetProcessId = { - type: "SET_PROCESS_ID" - payload: { - terminalId: Terminal["id"] - processId?: number - } -} - -type SetDetachedWindowOwner = { - type: "SET_DETACHED_WINDOW_OWNER" - payload: boolean -} - type SetWholeState = { type: "SET_WHOLE_STATE" payload: ReactRouterDevtoolsState @@ -287,32 +92,13 @@ type SetPersistOpenAction = { payload: boolean } -type SetServerInfo = { - type: "SET_SERVER_INFO" - payload: ServerInfo -} - -type SetHtmlErrors = { - type: "SET_HTML_ERRORS" - payload: HTMLError[] -} - /** Aggregate of all action types */ export type ReactRouterDevtoolsActions = | SetTimelineEvent - | ToggleTerminalLock - | AddOrRemoveTerminal - | AddTerminalOutput - | ClearTerminalOutput - | AddTerminalHistory - | SetProcessId | PurgeTimeline | SetSettings | SetWholeState - | SetDetachedWindowOwner | SetIsSubmittedAction - | SetServerInfo - | SetHtmlErrors | SetPersistOpenAction export const rdtReducer = ( @@ -320,21 +106,6 @@ export const rdtReducer = ( { type, payload }: ReactRouterDevtoolsActions ): ReactRouterDevtoolsState => { switch (type) { - case "SET_DETACHED_WINDOW_OWNER": - return { - ...state, - detachedWindowOwner: payload, - } - case "SET_HTML_ERRORS": - return { - ...state, - htmlErrors: [...payload], - } - case "SET_SERVER_INFO": - return { - ...state, - server: payload, - } case "SET_SETTINGS": return { ...state, @@ -368,96 +139,6 @@ export const rdtReducer = ( isSubmitted: true, } - case "SET_PROCESS_ID": - return { - ...state, - terminals: state.terminals.map((terminal) => { - if (terminal.id === payload.terminalId) { - return { - ...terminal, - processId: payload.processId, - } - } - return terminal - }), - } - case "TOGGLE_TERMINAL_LOCK": - return { - ...state, - terminals: state.terminals.map((terminal) => { - if (terminal.id === payload.terminalId) { - return { - ...terminal, - locked: payload.locked ?? !terminal.locked, - } - } - return terminal - }), - } - case "ADD_OR_REMOVE_TERMINAL": { - const terminalExists = state.terminals.some((terminal) => terminal.id === payload) - if (terminalExists) { - return { - ...state, - terminals: state.terminals - .filter((terminal) => terminal.id !== payload) - .map((terminal, i) => ({ ...terminal, id: i })), - } - } - return { - ...state, - terminals: [ - ...state.terminals, - { - id: state.terminals.length, - locked: false, - history: [], - output: [], - }, - ], - } - } - case "ADD_TERMINAL_OUTPUT": - return { - ...state, - terminals: state.terminals.map((terminal) => { - if (terminal.id === payload.terminalId) { - return { - ...terminal, - output: [...terminal.output, payload.output], - } - } - return terminal - }), - } - case "CLEAR_TERMINAL_OUTPUT": - return { - ...state, - terminals: state.terminals.map((terminal) => { - if (terminal.id === payload) { - return { - ...terminal, - output: [], - } - } - return terminal - }), - } - - case "ADD_TERMINAL_HISTORY": - return { - ...state, - terminals: state.terminals.map((terminal) => { - if (terminal.id === payload.terminalId) { - return { - ...terminal, - history: [...terminal.history, payload.history], - } - } - return terminal - }), - } - case "SET_PERSIST_OPEN": return { ...state, diff --git a/packages/react-router-devtools/src/client/context/requests/request-context.tsx b/packages/react-router-devtools/src/client/context/requests/request-context.tsx index 99dede28..69c45dd6 100644 --- a/packages/react-router-devtools/src/client/context/requests/request-context.tsx +++ b/packages/react-router-devtools/src/client/context/requests/request-context.tsx @@ -1,39 +1,57 @@ import { type ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react" +import { eventClient } from "../../../shared/event-client" import type { RequestEvent } from "../../../shared/request-event" const RequestContext = createContext<{ requests: RequestEvent[] removeAllRequests: () => void -}>({ requests: [], removeAllRequests: () => {} }) + isLimitReached: boolean +}>({ requests: [], removeAllRequests: () => {}, isLimitReached: false }) const requestMap = new Map() +const MAX_REQUESTS = 60 export const RequestProvider = ({ children }: { children: ReactNode }) => { const [requests, setRequests] = useState([]) - const setNewRequests = useCallback((payload: string) => { - const requests = JSON.parse(payload) - const newRequests = Array.isArray(requests) ? requests : [requests] - for (const req of newRequests) { - requestMap.set(req.id + req.startTime, req) - import.meta.hot?.send("remove-event", { ...req, fromClient: true }) + const [isLimitReached, setIsLimitReached] = useState(false) + + const handleRequestEvent = useCallback((event: { payload: RequestEvent }) => { + const req = event.payload + requestMap.set(req.id + req.startTime, req) + + // Get all requests and sort by start time (oldest first) + const allRequests = Array.from(requestMap.values()).sort((a, b) => a.startTime - b.startTime) + + // If we exceed MAX_REQUESTS, remove the oldest ones + if (allRequests.length > MAX_REQUESTS) { + const requestsToRemove = allRequests.slice(0, allRequests.length - MAX_REQUESTS) + for (const oldRequest of requestsToRemove) { + requestMap.delete(oldRequest.id + oldRequest.startTime) + } + setIsLimitReached(true) } + setRequests(Array.from(requestMap.values())) }, []) + useEffect(() => { - import.meta.hot?.send("get-events") - import.meta.hot?.on("get-events", setNewRequests) - import.meta.hot?.on("request-event", setNewRequests) + const unsubscribeRequestEvent = eventClient.on("request-event", handleRequestEvent) + return () => { - import.meta.hot?.off?.("get-events", setNewRequests) - import.meta.hot?.off?.("request-event", setNewRequests) + unsubscribeRequestEvent() } - }, [setNewRequests]) + }, [handleRequestEvent]) const removeAllRequests = useCallback(() => { setRequests([]) + setIsLimitReached(false) requestMap.clear() }, []) - return {children} + return ( + + {children} + + ) } export const useRequestContext = () => { diff --git a/packages/react-router-devtools/src/client/context/terminal/types.ts b/packages/react-router-devtools/src/client/context/terminal/types.ts deleted file mode 100644 index f61bdfe7..00000000 --- a/packages/react-router-devtools/src/client/context/terminal/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -interface TerminalOutput { - type: "output" | "command" | "error" - value: string -} - -export interface Terminal { - id: number - locked: boolean - history: string[] - output: TerminalOutput[] - processId?: number -} diff --git a/packages/react-router-devtools/src/client/context/useRDTContext.ts b/packages/react-router-devtools/src/client/context/useRDTContext.ts index d4d16711..4946a27d 100644 --- a/packages/react-router-devtools/src/client/context/useRDTContext.ts +++ b/packages/react-router-devtools/src/client/context/useRDTContext.ts @@ -24,65 +24,6 @@ const useRDTContext = () => { } } -export const useHtmlErrors = () => { - const { state, dispatch } = useRDTContext() - const { htmlErrors } = state - const setHtmlErrors = useCallback( - (htmlErrors: ReactRouterDevtoolsState["htmlErrors"]) => { - dispatch({ - type: "SET_HTML_ERRORS", - payload: htmlErrors, - }) - }, - [dispatch] - ) - return { htmlErrors, setHtmlErrors } -} - -export const useServerInfo = () => { - const { state, dispatch } = useRDTContext() - const { server } = state - const setServerInfo = useCallback( - (serverInfo: Partial) => { - dispatch({ - type: "SET_SERVER_INFO", - payload: { - ...server, - ...serverInfo, - routes: { - ...server?.routes, - ...serverInfo?.routes, - }, - }, - }) - }, - [dispatch, server] - ) - return { server, setServerInfo } -} - -export const useDetachedWindowControls = () => { - const { state, dispatch } = useRDTContext() - const { detachedWindow, detachedWindowOwner } = state - - const setDetachedWindowOwner = useCallback( - (isDetachedWindowOwner: boolean) => { - dispatch({ - type: "SET_DETACHED_WINDOW_OWNER", - payload: isDetachedWindowOwner, - }) - }, - [dispatch] - ) - - return { - detachedWindow: detachedWindow || window.RDT_MOUNTED, - detachedWindowOwner, - setDetachedWindowOwner, - isDetached: detachedWindow || detachedWindowOwner, - } -} - export const useSettingsContext = () => { const { dispatch, state } = useRDTContext() const { settings } = state @@ -98,20 +39,6 @@ export const useSettingsContext = () => { return { setSettings, settings } } -export const usePersistOpen = () => { - const { dispatch, state } = useRDTContext() - const { persistOpen } = state - const setPersistOpen = useCallback( - (persistOpen: boolean) => { - dispatch({ - type: "SET_PERSIST_OPEN", - payload: persistOpen, - }) - }, - [dispatch] - ) - return { persistOpen, setPersistOpen } -} /** * Returns an object containing functions and state related to the timeline context. * @returns {Object} An object containing the following properties: @@ -134,5 +61,3 @@ export const useTimelineContext = () => { return { setTimelineEvent, timeline, clearTimeline } } - -export { useRDTContext } diff --git a/packages/react-router-devtools/src/client/embedded-dev-tools.tsx b/packages/react-router-devtools/src/client/embedded-dev-tools.tsx index adf65843..6b830439 100644 --- a/packages/react-router-devtools/src/client/embedded-dev-tools.tsx +++ b/packages/react-router-devtools/src/client/embedded-dev-tools.tsx @@ -1,39 +1,38 @@ import clsx from "clsx" import { useEffect, useState } from "react" -import { useLocation } from "react-router" import { RDTContextProvider } from "./context/RDTContext.js" -import { useSettingsContext } from "./context/useRDTContext.js" -import { useReactTreeListeners } from "./hooks/useReactTreeListeners.js" +import { useFindRouteOutlets } from "./hooks/useReactTreeListeners.js" import { useSetRouteBoundaries } from "./hooks/useSetRouteBoundaries.js" import { useTimelineHandler } from "./hooks/useTimelineHandler.js" import { ContentPanel } from "./layout/ContentPanel.js" import { MainPanel } from "./layout/MainPanel.js" import { Tabs } from "./layout/Tabs.js" import type { ReactRouterDevtoolsProps } from "./react-router-dev-tools.js" +// Import to ensure global reset styles are injected +import "./styles/use-styles.js" +import { RequestProvider } from "./context/requests/request-context.js" import { REACT_ROUTER_DEV_TOOLS } from "./utils/storage.js" export interface EmbeddedDevToolsProps extends ReactRouterDevtoolsProps { mainPanelClassName?: string className?: string } -const Embedded = ({ plugins: pluginArray, mainPanelClassName, className }: EmbeddedDevToolsProps) => { +const Embedded = ({ mainPanelClassName, className }: EmbeddedDevToolsProps) => { useTimelineHandler() - useReactTreeListeners() + useFindRouteOutlets() useSetRouteBoundaries() - const { settings } = useSettingsContext() - const { position } = settings - const leftSideOriented = position.includes("left") - const url = useLocation().search - const plugins = pluginArray?.map((plugin) => (typeof plugin === "function" ? plugin() : plugin)) - if (settings.requireUrlFlag && !url.includes(settings.urlFlag)) return null + return (
- - + +
) @@ -52,14 +51,16 @@ function useHydrated() { return hydrated } -const EmbeddedDevTools = ({ plugins, mainPanelClassName, className }: EmbeddedDevToolsProps) => { +const EmbeddedDevTools = ({ config, mainPanelClassName, className }: EmbeddedDevToolsProps) => { const hydrated = useHydrated() if (!hydrated) return null return ( - - + + + + ) } diff --git a/packages/react-router-devtools/src/client/hof.ts b/packages/react-router-devtools/src/client/hof.ts index 5cf680e2..7b5b4789 100644 --- a/packages/react-router-devtools/src/client/hof.ts +++ b/packages/react-router-devtools/src/client/hof.ts @@ -1,12 +1,13 @@ import type { ClientActionFunctionArgs, ClientLoaderFunctionArgs, LinksFunction } from "react-router" import { convertBigIntToString } from "../shared/bigint-util" +import { eventClient } from "../shared/event-client" import type { RequestEvent } from "../shared/request-event" const sendEventToDevServer = (req: RequestEvent) => { if (req.data) { req.data = convertBigIntToString(req.data) } - import.meta.hot?.send("request-event", req) + eventClient.emit("request-event", req) } const analyzeClientLoaderOrAction = ( @@ -24,6 +25,7 @@ const analyzeClientLoaderOrAction = ( headers, startTime, id: routeId, + routeId: routeId, method: args.request.method, }) let aborted = false @@ -36,6 +38,7 @@ const analyzeClientLoaderOrAction = ( startTime, endTime: Date.now(), id: routeId, + routeId: routeId, method: args.request.method, aborted: true, }) @@ -50,6 +53,7 @@ const analyzeClientLoaderOrAction = ( startTime, endTime: Date.now(), id: routeId, + routeId: routeId, data, method: args.request.method, }) @@ -72,3 +76,57 @@ export const withLinksWrapper = (links: LinksFunction, rdtStylesheet: string): L export const withClientActionWrapper = (clientAction: (args: ClientActionFunctionArgs) => any, routeId: string) => { return analyzeClientLoaderOrAction(clientAction, routeId, "client-action") } + +// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type +const analyzeClientMiddleware = (middleware: any, routeId: string, index: number, middlewareName?: string) => { + // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type + return async (args: any, next: any) => { + const startTime = Date.now() + const name = middlewareName || middleware.name || `Anonymous ${index}` + + sendEventToDevServer({ + type: "client-middleware", + url: args.request.url, + headers: Object.fromEntries(args.request.headers.entries()), + startTime, + id: routeId, + routeId: routeId, + method: args.request.method, + middlewareName: name, + middlewareIndex: index, + }) + + const result = await middleware(args, next) + + sendEventToDevServer({ + type: "client-middleware", + url: args.request.url, + headers: Object.fromEntries(args.request.headers.entries()), + startTime, + endTime: Date.now(), + id: routeId, + routeId: routeId, + method: args.request.method, + middlewareName: name, + middlewareIndex: index, + }) + + return result + } +} + +// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type +export const withClientMiddlewareWrapper = (middlewares: any[], routeId: string) => { + return middlewares.map((middleware, index) => analyzeClientMiddleware(middleware, routeId, index)) +} + +// Single middleware wrapper for use by AST transformation +export const withClientMiddlewareWrapperSingle = ( + // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type + middleware: any, + routeId: string, + index: number, + middlewareName: string +) => { + return analyzeClientMiddleware(middleware, routeId, index, middlewareName) +} diff --git a/packages/react-router-devtools/src/client/hooks/detached/useCheckIfStillDetached.ts b/packages/react-router-devtools/src/client/hooks/detached/useCheckIfStillDetached.ts deleted file mode 100644 index 02b1d1aa..00000000 --- a/packages/react-router-devtools/src/client/hooks/detached/useCheckIfStillDetached.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useCallback, useContext, useEffect, useState } from "react" -import { RDTContext, getExistingStateFromStorage } from "../../context/RDTContext.js" -import { - REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, - REACT_ROUTER_DEV_TOOLS_DETACHED, - REACT_ROUTER_DEV_TOOLS_DETACHED_OWNER, - REACT_ROUTER_DEV_TOOLS_IS_DETACHED, - getBooleanFromStorage, - setStorageItem, -} from "../../utils/storage.js" - -export const useCheckIfStillDetached = () => { - const { dispatch } = useContext(RDTContext) - const [checking, setChecking] = useState(false) - const isDetached = getBooleanFromStorage(REACT_ROUTER_DEV_TOOLS_IS_DETACHED) - - useEffect(() => { - if (!checking || !isDetached) { - return - } - - // On reload the detached window will set the flag back to false so we can check if it is still detached - const isNotDetachedAnymore = getBooleanFromStorage(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED) - // The window hasn't set it back to true so it is not detached anymore and we clean all the detached state - if (isNotDetachedAnymore) { - setStorageItem(REACT_ROUTER_DEV_TOOLS_IS_DETACHED, "false") - setStorageItem(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, "false") - sessionStorage.removeItem(REACT_ROUTER_DEV_TOOLS_DETACHED_OWNER) - sessionStorage.removeItem(REACT_ROUTER_DEV_TOOLS_DETACHED) - const state = getExistingStateFromStorage() - dispatch({ type: "SET_WHOLE_STATE", payload: state }) - setChecking(false) - } - }, [checking, dispatch, isDetached]) - - const checkDetachment = useCallback( - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the event type - (e: any) => { - // We only care about the should_check key - if (e.key !== REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED) { - return - } - - const shouldCheckDetached = getBooleanFromStorage(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED) - - // If the detached window is unloaded we want to check if it is still there - if (shouldCheckDetached && !checking) { - setTimeout(() => setChecking(true), 200) - } - }, - [checking] - ) - useEffect(() => { - if (checking || !isDetached) { - return - } - - addEventListener("storage", checkDetachment) - return () => removeEventListener("storage", checkDetachment) - }, [checking, isDetached, checkDetachment]) -} diff --git a/packages/react-router-devtools/src/client/hooks/detached/useListenToRouteChange.ts b/packages/react-router-devtools/src/client/hooks/detached/useListenToRouteChange.ts deleted file mode 100644 index cd78ce11..00000000 --- a/packages/react-router-devtools/src/client/hooks/detached/useListenToRouteChange.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useRef } from "react" -import { useLocation, useNavigate, useNavigation } from "react-router" -import { detachedModeSetup } from "../../context/RDTContext.js" -import { useDetachedWindowControls } from "../../context/useRDTContext.js" -import { getStorageItem, setStorageItem } from "../../utils/storage.js" -import { useAttachListener } from "../useAttachListener.js" - -const LOCAL_STORAGE_ROUTE_KEY = "rdt_route" - -export const setRouteInLocalStorage = (route: string) => setStorageItem(LOCAL_STORAGE_ROUTE_KEY, route) - -const getRouteFromLocalStorage = () => getStorageItem(LOCAL_STORAGE_ROUTE_KEY) - -export const useListenToRouteChange = () => { - const { detachedWindowOwner } = useDetachedWindowControls() - const location = useLocation() - const navigate = useNavigate() - const navigation = useNavigation() - const locationRoute = location.pathname + location.search - const navigationRoute = (navigation.location?.pathname ?? "") + (navigation.location?.search ?? "") - const ref = useRef(locationRoute) - const route = getRouteFromLocalStorage() - - // Used by the owner window only - // biome-ignore lint/correctness/useExhaustiveDependencies: investigate if needed - useEffect(() => { - const { detachedWindowOwner } = detachedModeSetup() - if (!detachedWindowOwner) { - return - } - // If the route changes and this is the original window store the event into local storage - if (route !== locationRoute) { - setRouteInLocalStorage(locationRoute) - } - }, [locationRoute, detachedWindowOwner, route]) - - // Used to sync the route between the routes - - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the event type (should be fixed) - useAttachListener("storage", "window", (e: any) => { - // We only care about the key that changes the route - if (e.key !== LOCAL_STORAGE_ROUTE_KEY) { - return - } - - const route = getRouteFromLocalStorage() - - if (route && route !== ref.current && route !== navigationRoute && navigation.state === "idle") { - ref.current = route - navigate(route) - } - }) -} diff --git a/packages/react-router-devtools/src/client/hooks/detached/useRemoveBody.ts b/packages/react-router-devtools/src/client/hooks/detached/useRemoveBody.ts deleted file mode 100644 index 13fbbe79..00000000 --- a/packages/react-router-devtools/src/client/hooks/detached/useRemoveBody.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from "react" -import type { ReactRouterDevtoolsState } from "../../context/rdtReducer.js" -import { REACT_ROUTER_DEV_TOOLS } from "../../utils/storage.js" - -export const useRemoveBody = (state: ReactRouterDevtoolsState) => { - useEffect(() => { - if (!state.detachedWindow) { - return - } - - const elements = document.body.children - document.body.classList.add("bg-[#212121]") - for (let i = 0; i < elements.length; i++) { - const element = elements[i] - if (element.id !== REACT_ROUTER_DEV_TOOLS) { - element.classList.add("hidden") - } - } - }, [state]) -} diff --git a/packages/react-router-devtools/src/client/hooks/detached/useResetDetachmentCheck.ts b/packages/react-router-devtools/src/client/hooks/detached/useResetDetachmentCheck.ts deleted file mode 100644 index f2ae4587..00000000 --- a/packages/react-router-devtools/src/client/hooks/detached/useResetDetachmentCheck.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useDetachedWindowControls } from "../../context/useRDTContext.js" -import { REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, setStorageItem } from "../../utils/storage.js" -import { useAttachListener } from "../useAttachListener.js" -import { useCheckIfStillDetached } from "./useCheckIfStillDetached.js" - -export const useResetDetachmentCheck = () => { - const { isDetached } = useDetachedWindowControls() - useCheckIfStillDetached() - useAttachListener("unload", "window", () => setStorageItem(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, "true"), isDetached) -} diff --git a/packages/react-router-devtools/src/client/hooks/detached/useSyncStateWhenDetached.ts b/packages/react-router-devtools/src/client/hooks/detached/useSyncStateWhenDetached.ts deleted file mode 100644 index 96bf48f5..00000000 --- a/packages/react-router-devtools/src/client/hooks/detached/useSyncStateWhenDetached.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getExistingStateFromStorage } from "../../context/RDTContext.js" -import { useRDTContext } from "../../context/useRDTContext.js" -import { REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE } from "../../utils/storage.js" -import { useAttachListener } from "../useAttachListener.js" - -const refreshRequiredKeys = [REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE] - -export const useSyncStateWhenDetached = () => { - const { dispatch, state } = useRDTContext() - - // biome-ignore lint/suspicious/noExplicitAny: this should be fixed - useAttachListener("storage", "window", (e: any) => { - // Not in detached mode - if (!state.detachedWindow && !state.detachedWindowOwner) { - return - } - // Not caused by the dev tools - if (!refreshRequiredKeys.includes(e.key)) { - return - } - // Check if the settings have not changed and early return - if (e.key === REACT_ROUTER_DEV_TOOLS_SETTINGS) { - const oldSettings = JSON.stringify(state.settings) - if (oldSettings === e.newValue) { - return - } - } - // Check if the state has not changed and early return - if (e.key === REACT_ROUTER_DEV_TOOLS_STATE) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { settings, ...rest } = state - const oldState = JSON.stringify(rest) - if (oldState === e.newValue) { - return - } - } - - // store new state - const newState = getExistingStateFromStorage() - dispatch({ type: "SET_WHOLE_STATE", payload: newState }) - }) -} diff --git a/packages/react-router-devtools/src/client/hooks/useAttachListener.ts b/packages/react-router-devtools/src/client/hooks/useAttachListener.ts deleted file mode 100644 index 27c1eefb..00000000 --- a/packages/react-router-devtools/src/client/hooks/useAttachListener.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useEffect, useRef } from "react" - -type ListenerAttachmentTarget = "window" | "document" | "body" - -const getAttachment = (target: ListenerAttachmentTarget) => { - switch (target) { - case "window": - return typeof window !== "undefined" ? window : null - case "document": - return typeof document !== "undefined" ? document : null - case "body": - return typeof document !== "undefined" ? document.body : null - } -} - -/** - * Helper hook that listens to the document scroll event and triggers a callback function - * @param fn Function to be called when the event happens - */ -export const useAttachListener = ( - listener: keyof HTMLElementEventMap | keyof WindowEventMap | keyof DocumentEventMap, - attachTarget: ListenerAttachmentTarget, - fn: EventListener, - shouldAttach = true -) => useAttachListenerToNode(listener, getAttachment(attachTarget), fn, shouldAttach) - -/** - * Helper hook that listens to the provided event on the provided node and triggers a callback function - * @param fn Function to be called when the event happens - */ -const useAttachListenerToNode = ( - listener: keyof HTMLElementEventMap | keyof WindowEventMap | keyof DocumentEventMap, - node: HTMLElement | Window | Document | null, - fn: EventListener, - shouldAttach = true -) => { - const callbackRef = useRef(fn) - // Makes sure the latest function callback is used so it doesn't use stale values and props - useEffect(() => { - callbackRef.current = fn - }) - // Attaches the event listener to the node - useEffect(() => { - if (!shouldAttach) return - node?.addEventListener(listener, (e) => callbackRef.current(e)) - return () => node?.removeEventListener(listener, (e) => callbackRef.current(e)) - }, [listener, node, shouldAttach]) -} - -/** - * Helper hook that listens to the document scroll event and triggers a callback function - * @param fn Function to be called when the user scrolls - */ -export const useAttachWindowListener = ( - listener: keyof WindowEventMap, - fn: (...args: unknown[]) => unknown, - shouldAttach = true -) => { - return useAttachListener(listener, "window", fn, shouldAttach) -} - -/** - * Helper hook that listens to the document scroll event and triggers a callback function - * @param fn Function to be called when the user scrolls - */ -export const useAttachDocumentListener = ( - listener: keyof WindowEventMap, - fn: (...args: unknown[]) => unknown, - shouldAttach = true -) => { - return useAttachListener(listener, "document", fn, shouldAttach) -} - -/** - * Helper hook that listens to the document scroll event and triggers a callback function - * @param fn Function to be called when the user scrolls - */ -// const useAttachBodyListener = ( -// listener: keyof WindowEventMap, -// fn: (...args: unknown[]) => unknown, -// shouldAttach = true -//) => { -// return useAttachListener(listener, "body", fn, shouldAttach); -//}; diff --git a/packages/react-router-devtools/src/client/hooks/useCountdown.ts b/packages/react-router-devtools/src/client/hooks/useCountdown.ts deleted file mode 100644 index 727d3f04..00000000 --- a/packages/react-router-devtools/src/client/hooks/useCountdown.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect, useState } from "react" - -const getTimeLeft = (countDown: number) => { - // calculate time left - const days = Math.floor(countDown / (1000 * 60 * 60 * 24)) - const hours = Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)) - const seconds = Math.floor((countDown % (1000 * 60)) / 1000) - - return { days, hours, minutes, seconds } -} - -const useCountdown = (targetDate: string | Date) => { - const countDownDate = new Date(targetDate).getTime() - - const [countDown, setCountDown] = useState(countDownDate - new Date().getTime()) - - // biome-ignore lint/correctness/useExhaustiveDependencies: investigate - useEffect(() => { - const timeLeft = getTimeLeft(countDown) - if (timeLeft.seconds <= 0) { - return - } - const interval = setInterval(() => { - setCountDown(countDownDate - new Date().getTime()) - }, 1000) - - return () => clearInterval(interval) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [countDownDate]) - - const timeLeft = getTimeLeft(countDown) - const stringRepresentation = `${timeLeft.days > 0 ? `${timeLeft.days}d ` : ""}${ - timeLeft.hours ? `${timeLeft.hours}h ` : "" - }${timeLeft.minutes ? `${timeLeft.minutes}m ` : ""}${timeLeft.seconds ? `${timeLeft.seconds}s` : ""}` - return { ...timeLeft, stringRepresentation } -} - -export { useCountdown } diff --git a/packages/react-router-devtools/src/client/hooks/useDebounce.ts b/packages/react-router-devtools/src/client/hooks/useDebounce.ts deleted file mode 100644 index 92ef23c9..00000000 --- a/packages/react-router-devtools/src/client/hooks/useDebounce.ts +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react" - -// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -function debounce(func: (...args: any[]) => any, timeout = 300) { - let timer: NodeJS.Timeout - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - return (...args: any[]) => { - clearTimeout(timer) - timer = setTimeout(() => { - /* @ts-ignore */ - func.apply(this, args) - }, timeout) - } -} - -// biome-ignore lint/suspicious/noExplicitAny: we don't care about the type -export function useDebounce(callback: (...args: any[]) => void, delay = 300) { - const callbackRef = React.useRef(callback) - React.useEffect(() => { - callbackRef.current = callback - }) - return React.useMemo(() => debounce((...args) => callbackRef.current(...args), delay), [delay]) -} diff --git a/packages/react-router-devtools/src/client/hooks/useDevServerConnection.ts b/packages/react-router-devtools/src/client/hooks/useDevServerConnection.ts deleted file mode 100644 index 889c0e06..00000000 --- a/packages/react-router-devtools/src/client/hooks/useDevServerConnection.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect } from "react" -import { useNavigation } from "react-router" -import type { ActionEvent, LoaderEvent } from "../../server/event-queue.js" -import type { ServerInfo } from "../context/rdtReducer.js" -import { useServerInfo } from "../context/useRDTContext.js" -import { cutArrayToLastN } from "../utils/common.js" - -const updateRouteInfo = ( - server: ServerInfo | undefined, - routes: ServerInfo["routes"], - event: LoaderEvent | ActionEvent, - includeServerInfo = true -) => { - const { data, type } = event - const { id, ...rest } = data - // Get existing route - const existingRouteInfo = !includeServerInfo ? routes?.[id] : (routes?.[id] ?? server?.routes?.[id]) - let newRouteData = [...(existingRouteInfo?.[type === "loader" ? "loaders" : "actions"] || []), rest] - // Makes sure there are no more than 20 entries per loader/action - newRouteData = cutArrayToLastN(newRouteData, 20) - // Calculates the min, max and average execution time - const { min, max, total } = newRouteData.reduce( - (acc, dataPiece) => { - return { - min: Math.min(acc.min, dataPiece.executionTime), - max: Math.max(acc.max, dataPiece.executionTime), - total: acc.total + dataPiece.executionTime, - } - }, - { min: 100000, max: 0, total: 0 } - ) - - const loaderTriggerCount = existingRouteInfo?.loaderTriggerCount || 0 - const actionTriggerCount = existingRouteInfo?.actionTriggerCount || 0 - // Updates the route info with the new data - // biome-ignore lint/style/noNonNullAssertion: - routes![id] = { - ...existingRouteInfo, - lowestExecutionTime: min, - highestExecutionTime: max, - averageExecutionTime: Number(Number(total / newRouteData.length).toFixed(2)), - loaderTriggerCount: type === "loader" ? loaderTriggerCount + 1 : loaderTriggerCount, - loaders: type === "loader" ? newRouteData : (existingRouteInfo?.loaders ?? []), - actions: type === "action" ? newRouteData : (existingRouteInfo?.actions ?? []), - lastLoader: type === "loader" ? rest : (existingRouteInfo?.lastLoader ?? {}), - lastAction: type === "action" ? rest : (existingRouteInfo?.lastAction ?? {}), - actionTriggerCount: type === "action" ? actionTriggerCount + 1 : actionTriggerCount, - } -} - -const useDevServerConnection = () => { - const navigation = useNavigation() - const { server, setServerInfo } = useServerInfo() - - // Pull the event queue from the server when the page is idle - useEffect(() => { - if (typeof import.meta.hot === "undefined") return - if (navigation.state !== "idle") return - // We send a pull & clear event to pull the event queue and clear it - import.meta.hot.send("all-route-info") - }, [navigation.state]) - - useEffect(() => { - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - const cb2 = (data: any) => { - const events = JSON.parse(data).data - const routes: ServerInfo["routes"] = {} - for (const routeInfo of Object.values(events)) { - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - const { loader, action } = routeInfo as any - const events = [ - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - loader.map((e: any) => ({ type: "loader", data: e })), - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - action.map((e: any) => ({ type: "action", data: e })), - ].flat() - for (const event of events) { - updateRouteInfo(server, routes, event, false) - } - } - - setServerInfo({ routes }) - } - if (typeof import.meta.hot !== "undefined") { - import.meta.hot.on("all-route-info", cb2) - } - - return () => { - if (typeof import.meta.hot !== "undefined") { - import.meta.hot.dispose(cb2) - } - } - }, [server, setServerInfo]) - - const isConnected = typeof import.meta.hot !== "undefined" - - return { - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type - sendJsonMessage: (data: any) => import.meta.hot?.send(data.type, data), - connectionStatus: "Open" as const, - isConnected, - } -} - -export { useDevServerConnection } diff --git a/packages/react-router-devtools/src/client/hooks/useOnWindowResize.ts b/packages/react-router-devtools/src/client/hooks/useOnWindowResize.ts deleted file mode 100644 index fc030903..00000000 --- a/packages/react-router-devtools/src/client/hooks/useOnWindowResize.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useState } from "react" - -type WindowSize = { - width: number - height: number -} -export const useOnWindowResize = () => { - const [windowSize, setWindowSize] = useState({ - width: window.innerWidth, - height: window.innerHeight, - }) - - useEffect(() => { - const handleResize = () => { - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }) - } - - window.addEventListener("resize", handleResize) - - return () => { - window.removeEventListener("resize", handleResize) - } - }, []) - return windowSize -} diff --git a/packages/react-router-devtools/src/client/hooks/useOpenElementSource.ts b/packages/react-router-devtools/src/client/hooks/useOpenElementSource.ts deleted file mode 100644 index d3b41778..00000000 --- a/packages/react-router-devtools/src/client/hooks/useOpenElementSource.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useAttachDocumentListener } from "./useAttachListener.js" -import { useDevServerConnection } from "./useDevServerConnection.js" - -const useOpenElementSource = () => { - const { sendJsonMessage } = useDevServerConnection() - - // biome-ignore lint/suspicious/noExplicitAny: this should be fixed - useAttachDocumentListener("contextmenu", (e: any) => { - if (!e.shiftKey || !e) { - return - } - - e.stopPropagation() - e.preventDefault() - const target = e.target as HTMLElement - const rdtSource = target?.getAttribute("data-rrdt-source") - - if (rdtSource) { - const [source, line, column] = rdtSource.split(":") - return sendJsonMessage({ - type: "open-source", - data: { source, line, column }, - }) - } - for (const key in e.target) { - if (key.startsWith("__reactFiber")) { - // biome-ignore lint/suspicious/noExplicitAny: we don't know the type (this should be fixed) - const fiberNode = (e.target as any)[key] - - const originalSource = fiberNode?._debugSource - const source = fiberNode?._debugOwner?._debugSource ?? fiberNode?._debugSource - const line = source?.fileName?.startsWith("/") ? originalSource?.lineNumber : source?.lineNumber - const fileName = source?.fileName?.startsWith("/") ? originalSource?.fileName : source?.fileName - if (fileName && line) { - return sendJsonMessage({ - type: "open-source", - data: { source: fileName, line, column: 0 }, - }) - } - } - } - }) -} - -export { useOpenElementSource } diff --git a/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts b/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts index 6438d96a..df2e922e 100644 --- a/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts +++ b/packages/react-router-devtools/src/client/hooks/useReactTreeListeners.ts @@ -1,169 +1,47 @@ -import { type Fiber, onCommitFiberRoot, traverseFiber } from "bippy" -import { useCallback, useEffect, useRef } from "react" +import { useCallback, useEffect } from "react" import { useNavigation } from "react-router" -import type { HTMLError } from "../context/rdtReducer.js" -import { useHtmlErrors } from "../context/useRDTContext.js" export const ROUTE_CLASS = "outlet-route" -export function useReactTreeListeners() { - const invalidHtmlCollection = useRef([]) - const { setHtmlErrors } = useHtmlErrors() - const addToInvalidCollection = (entry: HTMLError) => { - if (invalidHtmlCollection.current.find((item) => JSON.stringify(item) === JSON.stringify(entry))) return - invalidHtmlCollection.current.push(entry) - } - +export function useFindRouteOutlets() { const navigation = useNavigation() - // biome-ignore lint/suspicious/noExplicitAny: we don't know the type - const styleNearestElement = useCallback((fiberNode: Fiber | null) => { - if (!fiberNode) return + // biome-ignore lint/suspicious/noExplicitAny: we don't know the type + const traverseComponentTree = useCallback((fiberNode: any, callback: any) => { + callback(fiberNode) - if (fiberNode.stateNode) { - return fiberNode.stateNode?.classList?.add(ROUTE_CLASS) + let child = fiberNode.child + while (child) { + traverseComponentTree(child, callback) + child = child.sibling } - styleNearestElement(fiberNode?.child) }, []) - // biome-ignore lint/correctness/useExhaustiveDependencies: we want this to run only once - const findIncorrectHtml = useCallback( - // biome-ignore lint/suspicious/noExplicitAny: we don't know the type - (fiberNode: Fiber | null, originalFiberNode: Fiber | null, originalTag: string) => { - if (!fiberNode) return - - const tag = fiberNode.elementType - const addInvalid = () => { - const parentSource = originalFiberNode?._debugOwner?._debugSource ?? originalFiberNode?._debugSource - const source = fiberNode?._debugOwner?._debugSource ?? fiberNode?._debugSource - addToInvalidCollection({ - child: { - file: parentSource?.fileName, - tag: tag, - }, - parent: { - file: source?.fileName, - tag: originalTag, - }, - }) - } + // biome-ignore lint/suspicious/noExplicitAny: we don't know the type + const styleNearestElement = useCallback((fiberNode: any) => { + if (!fiberNode) return - if (originalTag === "a") { - const element = fiberNode.stateNode as HTMLElement - switch (tag) { - case "a": - case "button": - case "details": - case "embed": - case "iframe": - case "label": - case "select": - case "textarea": { - addInvalid() - break - } - case "audio": { - if (element.getAttribute("controls") !== null) { - addInvalid() - } - break - } - case "img": { - if (element.getAttribute("usemap") !== null) { - addInvalid() - } - break - } - case "input": { - if (element.getAttribute("type") !== "hidden") { - addInvalid() - } - break - } - case "object": { - if (element.getAttribute("usemap") !== null) { - addInvalid() - } - break - } - case "video": { - if (element.getAttribute("controls") !== null) { - addInvalid() - } - break - } - default: { - break - } - } - } - if (originalTag === "p") { - switch (tag) { - case "div": - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - case "main": - case "pre": - case "p": - case "section": - case "table": - case "ul": - case "ol": - case "li": { - addInvalid() - break - } - default: { - break - } - } - } - if (originalTag === "form") { - if (tag === "form") { - addInvalid() - } - } - if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(originalTag)) { - if (tag === "h1" || tag === "h2" || tag === "h3" || tag === "h4" || tag === "h5" || tag === "h6") { - addInvalid() - } - } - findIncorrectHtml(fiberNode?.child, originalFiberNode, originalTag) - if (fiberNode?.sibling) { - findIncorrectHtml(fiberNode?.sibling, originalFiberNode, originalTag) - } - }, - [] - ) + if (fiberNode.stateNode) { + return fiberNode.stateNode.classList.add(ROUTE_CLASS) + } + styleNearestElement(fiberNode.child) + }, []) useEffect(() => { if (navigation.state !== "idle") return + // biome-ignore lint/suspicious/noExplicitAny: accessing React internals + const devTools = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ - onCommitFiberRoot((root) => - traverseFiber(root.current, (fiberNode) => { - if (fiberNode?.stateNode && fiberNode?.elementType === "form") { - findIncorrectHtml(fiberNode.child, fiberNode, "form") - } - if (fiberNode?.stateNode && fiberNode?.elementType === "a") { - findIncorrectHtml(fiberNode.child, fiberNode, "a") - } - if (fiberNode?.stateNode && fiberNode?.elementType === "p") { - findIncorrectHtml(fiberNode.child, fiberNode, "p") - } - if (fiberNode?.stateNode && ["h1", "h2", "h3", "h4", "h5", "h6"].includes(fiberNode?.elementType)) { - findIncorrectHtml(fiberNode.child, fiberNode, fiberNode?.elementType) - } - if (fiberNode?.elementType?.name === "default" || fiberNode?.elementType?.name === "RenderedRoute") { - styleNearestElement(fiberNode) - } - }) - ) - - setHtmlErrors(invalidHtmlCollection.current) - invalidHtmlCollection.current = [] - }, [navigation.state, styleNearestElement, findIncorrectHtml, setHtmlErrors]) + for (const [rendererID] of devTools.renderers) { + const fiberRoots = devTools.getFiberRoots(rendererID) + for (const rootFiber of fiberRoots) { + // biome-ignore lint/suspicious/noExplicitAny: we don't know the type + traverseComponentTree(rootFiber.current, (fiberNode: any) => { + if (fiberNode?.elementType?.name === "default" || fiberNode?.elementType?.name === "RenderedRoute") { + styleNearestElement(fiberNode) + } + }) + } + } + }, [navigation.state, styleNearestElement, traverseComponentTree]) } diff --git a/packages/react-router-devtools/src/client/hooks/useResize.ts b/packages/react-router-devtools/src/client/hooks/useResize.ts deleted file mode 100644 index aa199ed1..00000000 --- a/packages/react-router-devtools/src/client/hooks/useResize.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect, useState } from "react" -import { useSettingsContext } from "../context/useRDTContext.js" - -const useResize = () => { - const { setSettings, settings } = useSettingsContext() - const { height, maxHeight, minHeight, panelLocation } = settings - const [isResizing, setIsResizing] = useState(false) - - // biome-ignore lint/correctness/useExhaustiveDependencies: - const enableResize = useCallback(() => { - setIsResizing(true) - }, [setIsResizing]) - - // biome-ignore lint/correctness/useExhaustiveDependencies: - const disableResize = useCallback(() => { - setIsResizing(false) - }, [setIsResizing]) - - const resize = useCallback( - (e: MouseEvent) => { - if (isResizing) { - window.getSelection()?.removeAllRanges() // Prevent text selection - const newHeight = panelLocation === "top" ? e.clientY : window.innerHeight - e.clientY // Calculate the new height based on the mouse position - - //const newHeight = e.clientY; // You may want to add some offset here from props - - if (newHeight > maxHeight) { - setSettings({ height: maxHeight }) - return - } - - if (newHeight < minHeight) { - setSettings({ height: minHeight }) - return - } - - setSettings({ height: newHeight }) - } - }, - [isResizing, maxHeight, minHeight, setSettings, panelLocation] - ) - - useEffect(() => { - document.addEventListener("mousemove", resize) - document.addEventListener("mouseup", disableResize) - - return () => { - document.removeEventListener("mousemove", resize) - document.removeEventListener("mouseup", disableResize) - } - }, [disableResize, resize]) - - return { height, enableResize, disableResize, isResizing } -} - -export { useResize } diff --git a/packages/react-router-devtools/src/client/hooks/useSetRouteBoundaries.ts b/packages/react-router-devtools/src/client/hooks/useSetRouteBoundaries.ts index b0ebd391..3fa65eb4 100644 --- a/packages/react-router-devtools/src/client/hooks/useSetRouteBoundaries.ts +++ b/packages/react-router-devtools/src/client/hooks/useSetRouteBoundaries.ts @@ -1,20 +1,22 @@ import { useCallback, useEffect } from "react" import { useMatches } from "react-router" import { ROUTE_BOUNDARY_GRADIENTS } from "../context/rdtReducer.js" -import { useDetachedWindowControls, useSettingsContext } from "../context/useRDTContext.js" -import { useAttachListener } from "./useAttachListener.js" +import { useSettingsContext } from "../context/useRDTContext.js" +import { useStyles } from "../styles/use-styles.js" import { ROUTE_CLASS } from "./useReactTreeListeners.js" export const useSetRouteBoundaries = () => { const matches = useMatches() const { settings, setSettings } = useSettingsContext() - const { detachedWindow } = useDetachedWindowControls() + const { styles } = useStyles() + const applyOrRemoveClasses = useCallback( (isHovering?: boolean) => { // Overrides the hovering so the classes are force removed if needed const hovering = isHovering ?? settings.isHoveringRoute - // Classes to apply/remove - const classes = ["apply-tw", ROUTE_BOUNDARY_GRADIENTS[settings.routeBoundaryGradient]].join(" ") + // Get the Goober gradient class for the selected gradient + const gradientKey = ROUTE_BOUNDARY_GRADIENTS[settings.routeBoundaryGradient] + const gradientClass = styles.gradients[gradientKey as keyof typeof styles.gradients] const isRoot = settings.hoveredRoute === "root" // We get all the elements with this class name, the last one is the one we want because strict mode applies 2x divs @@ -27,45 +29,23 @@ export const useSetRouteBoundaries = () => { if (element) { // Root has no outlet so we need to use the body, otherwise we get the outlet that is the next sibling of the element const outlet = element - for (const classItem of classes.split(" ")) { - outlet.classList[hovering ? "add" : "remove"](classItem) + // Apply or remove the Goober gradient class + if (hovering) { + outlet.classList.add(gradientClass) + } else { + outlet.classList.remove(gradientClass) } } }, - [settings.hoveredRoute, settings.isHoveringRoute, settings.routeBoundaryGradient, matches.length] + [settings.hoveredRoute, settings.isHoveringRoute, settings.routeBoundaryGradient, matches.length, styles.gradients] ) - // Mouse left the document => remove classes => set isHovering to false so that detached mode removes as well - useAttachListener("mouseleave", "document", () => { - if (settings.showRouteBoundariesOn === "click") { - return - } - applyOrRemoveClasses() - if (!detachedWindow) { - return - } - setSettings({ - isHoveringRoute: false, - }) - }) - // Mouse is scrolling => remove classes => set isHovering to false so that detached mode removes as well - useAttachListener("wheel", "window", () => { - if (settings.showRouteBoundariesOn === "click") { - return - } - applyOrRemoveClasses(false) - if (!detachedWindow) { - return - } - setSettings({ - isHoveringRoute: false, - }) - }) + // We apply/remove classes on state change which happens in Page tab // biome-ignore lint/correctness/useExhaustiveDependencies: investigate useEffect(() => { if (!settings.isHoveringRoute && !settings.hoveredRoute) return applyOrRemoveClasses() - if (!settings.isHoveringRoute && !detachedWindow) { + if (!settings.isHoveringRoute) { setSettings({ hoveredRoute: "", isHoveringRoute: false, @@ -76,7 +56,6 @@ export const useSetRouteBoundaries = () => { settings.isHoveringRoute, settings.routeBoundaryGradient, applyOrRemoveClasses, - detachedWindow, setSettings, ]) } diff --git a/packages/react-router-devtools/src/client/hooks/useTabs.ts b/packages/react-router-devtools/src/client/hooks/useTabs.ts index 2ad6a008..00727322 100644 --- a/packages/react-router-devtools/src/client/hooks/useTabs.ts +++ b/packages/react-router-devtools/src/client/hooks/useTabs.ts @@ -1,25 +1,23 @@ import { useMemo } from "react" -import type { ReactRouterDevtoolsState } from "../context/rdtReducer.js" import { useSettingsContext } from "../context/useRDTContext.js" -import type { ReactRouterDevtoolsProps } from "../react-router-dev-tools.js" import { type Tab, tabs } from "../tabs/index.js" -import type { Tabs } from "../tabs/index.js" -const shouldHideTimeline = (activeTab: Tabs, tab: Tab | undefined, settings: ReactRouterDevtoolsState["settings"]) => { - if (activeTab === "routes" && settings.routeViewMode === "tree") return true +const shouldHideTimeline = (tab: Tab | undefined) => { return tab?.hideTimeline } -export const useTabs = (pluginsArray?: ReactRouterDevtoolsProps["plugins"]) => { +export const useTabs = () => { const { settings } = useSettingsContext() const { activeTab } = settings - const plugins = pluginsArray?.map((plugin) => (typeof plugin === "function" ? plugin() : plugin)) - const allTabs = useMemo(() => [...tabs, ...(plugins ? plugins : [])], [plugins]) + const allTabs = tabs const { Component, hideTimeline } = useMemo(() => { const tab = allTabs.find((tab) => tab.id === activeTab) - return { Component: tab?.component, hideTimeline: shouldHideTimeline(activeTab, tab, settings) } - }, [activeTab, allTabs, settings]) + return { + Component: tab?.component, + hideTimeline: shouldHideTimeline(tab), + } + }, [activeTab, allTabs]) return { visibleTabs: allTabs, @@ -27,6 +25,5 @@ export const useTabs = (pluginsArray?: ReactRouterDevtoolsProps["plugins"]) => { allTabs, hideTimeline, activeTab, - isPluginTab: !tabs.find((tab) => activeTab === tab.id), } } diff --git a/packages/react-router-devtools/src/client/hooks/useTimelineHandler.ts b/packages/react-router-devtools/src/client/hooks/useTimelineHandler.ts index 52fd2569..346d67a2 100644 --- a/packages/react-router-devtools/src/client/hooks/useTimelineHandler.ts +++ b/packages/react-router-devtools/src/client/hooks/useTimelineHandler.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react" import { useActionData, useFetchers, useNavigation } from "react-router" import type { TimelineEvent } from "../context/timeline/types.js" -import { useDetachedWindowControls, useTimelineContext } from "../context/useRDTContext.js" +import { useTimelineContext } from "../context/useRDTContext.js" const uniqueId = () => (Math.random() * Date.now()).toString() @@ -57,12 +57,7 @@ const useTimelineHandler = () => { const navigationEventQueue = useRef([]) const { setTimelineEvent } = useTimelineContext() const responseData = useActionData() - const { detachedWindow } = useDetachedWindowControls() useEffect(() => { - // Do not record events if the window is detached, the main window will handle it - if (detachedWindow) { - return - } const { state, location, formAction, formData, formMethod, formEncType } = navigation if (state === "idle") { @@ -134,7 +129,7 @@ const useTimelineHandler = () => { }) return } - }, [navigation, responseData, setTimelineEvent, detachedWindow]) + }, [navigation, responseData, setTimelineEvent]) const fetcherEventQueue = useRef([]) // Fetchers handler diff --git a/packages/react-router-devtools/src/client/init/hydration.ts b/packages/react-router-devtools/src/client/init/hydration.ts deleted file mode 100644 index daedf8eb..00000000 --- a/packages/react-router-devtools/src/client/init/hydration.ts +++ /dev/null @@ -1,63 +0,0 @@ -function removeStyleAndDataAttributes(inputString: string) { - // Define the regular expressions to match tags - const styleTagRegex = /]*>[\s\S]*?<\/style>/gi - const scriptTagRegex = /]*>[\s\S]*?<\/script>/gi - const templateRegex = /]*>[\s\S]*?<\/template>/gi - const styleRegex = /style="([^"]*)"/g - - let resultString = inputString - .replaceAll(styleTagRegex, "") - .replaceAll(scriptTagRegex, "") - .replaceAll(templateRegex, "") - .replaceAll("", "") - .replaceAll("", "") - - resultString = resultString.replaceAll(styleRegex, (_match, styleValue) => { - // Add a semicolon at the end of the style attribute if it doesn't already exist and remove spacing to remove false positives - const updatedStyle = styleValue.trim().endsWith(";") ? styleValue : `${styleValue};` - return `style="${updatedStyle.replaceAll(" ", "")}"` - }) - return resultString -} - -declare global { - interface Window { - HYDRATION_OVERLAY: { - SSR_HTML: string | undefined - CSR_HTML: string | undefined - ERROR: boolean | undefined - APP_ROOT_SELECTOR: string - } - } -} - -export const hydrationDetector = () => { - if (typeof window !== "undefined") { - if (!window.HYDRATION_OVERLAY) { - // biome-ignore lint/suspicious/noExplicitAny: we want to init the object - window.HYDRATION_OVERLAY = {} as any - } - window.addEventListener("error", (event) => { - const msg = event.message.toLowerCase() - - const isHydrationMsg = - msg.includes("hydration") || msg.includes("hydrating") || msg.includes("minified react error #418") - - if (isHydrationMsg) { - window.HYDRATION_OVERLAY.ERROR = true - - const appRootEl = document.querySelector("html") - - if (appRootEl) { - window.HYDRATION_OVERLAY.CSR_HTML = removeStyleAndDataAttributes(appRootEl.outerHTML) - } - } - }) - } - - const HYDRATION_OVERLAY_ELEMENT = typeof document !== "undefined" && document.querySelector("html") - - if (HYDRATION_OVERLAY_ELEMENT) { - window.HYDRATION_OVERLAY.SSR_HTML = removeStyleAndDataAttributes(HYDRATION_OVERLAY_ELEMENT.outerHTML) - } -} diff --git a/packages/react-router-devtools/src/client/init/root.tsx b/packages/react-router-devtools/src/client/init/root.tsx index d7849cce..3b25ef7b 100644 --- a/packages/react-router-devtools/src/client/init/root.tsx +++ b/packages/react-router-devtools/src/client/init/root.tsx @@ -1,22 +1,9 @@ -import { useEffect, useState } from "react" -import { createPortal } from "react-dom" +import type { ClientEventBusConfig, TanStackDevtoolsConfig } from "@tanstack/devtools" +import { TanStackDevtools, type TanStackDevtoolsReactPlugin } from "@tanstack/react-devtools" +import { Logo } from "../components/logo.js" import type { RdtClientConfig } from "../context/RDTContext.js" -import { RequestProvider } from "../context/requests/request-context.js" -import { ReactRouterDevTools, type ReactRouterDevtoolsProps } from "../react-router-dev-tools.js" -import { hydrationDetector } from "./hydration.js" - -let hydrating = true - -function useHydrated() { - const [hydrated, setHydrated] = useState(() => !hydrating) - - useEffect(function hydrate() { - hydrating = false - setHydrated(true) - }, []) - - return hydrated -} +import { EmbeddedDevTools } from "../embedded-dev-tools.js" +import { useStyles } from "../styles/use-styles.js" export const defineClientConfig = (config: RdtClientConfig) => config @@ -25,24 +12,50 @@ export const defineClientConfig = (config: RdtClientConfig) => config * @description Injects the dev tools into the Vite App, ONLY meant to be used by the package plugin, do not use this yourself! */ -// biome-ignore lint/suspicious/noExplicitAny: we don't know or care about props type -export const withViteDevTools = (Component: any, config?: ReactRouterDevtoolsProps) => (props: any) => { - hydrationDetector() - // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type here as we spread it below - function AppWithDevTools(props: any) { - const hydrated = useHydrated() - if (!hydrated) +type ViteDevToolsConfig = { + config?: RdtClientConfig + tanstackConfig?: Partial> + plugins?: Array + tanstackClientBusConfig?: Partial +} + +export const withViteDevTools = + ( + // biome-ignore lint/suspicious/noExplicitAny: we don't know or care about props type + Component: any, + viteConfig?: ViteDevToolsConfig + ) => + // biome-ignore lint/suspicious/noExplicitAny: we don't know or care about props type + (props: any) => { + // Extract config parts + const tanStackDevtoolsConfig = viteConfig?.tanstackConfig + const clientBusConfig = viteConfig?.tanstackClientBusConfig + // biome-ignore lint/suspicious/noExplicitAny: we don't care about the type here as we spread it below + function AppWithDevTools(props: any) { + const { styles } = useStyles() return ( - + <> - + + +
+ ), + }} + eventBusConfig={{ + ...clientBusConfig, + connectToServerBus: true, + }} + plugins={[ + { name: "React Router", render: , defaultOpen: true }, + ...(viteConfig?.plugins || []), + ]} + /> + ) - return ( - - - {createPortal(, document.body)} - - ) + } + return AppWithDevTools(props) } - return AppWithDevTools(props) -} diff --git a/packages/react-router-devtools/src/client/layout/ContentPanel.tsx b/packages/react-router-devtools/src/client/layout/ContentPanel.tsx index 48be792d..332ecfb5 100644 --- a/packages/react-router-devtools/src/client/layout/ContentPanel.tsx +++ b/packages/react-router-devtools/src/client/layout/ContentPanel.tsx @@ -1,34 +1,28 @@ -import clsx from "clsx" import { Fragment } from "react" import { useTabs } from "../hooks/useTabs.js" +import { cx } from "../styles/use-styles.js" +import { useStyles } from "../styles/use-styles.js" import { TimelineTab } from "../tabs/TimelineTab.js" -import type { Tab } from "../tabs/index.js" -interface ContentPanelProps { - leftSideOriented: boolean - plugins?: Tab[] -} - -const ContentPanel = ({ plugins }: ContentPanelProps) => { - const { Component, hideTimeline, isPluginTab, activeTab } = useTabs(plugins) +const ContentPanel = () => { + const { Component, hideTimeline, activeTab } = useTabs() + const { styles } = useStyles() return ( -
+
- {Component} + {Component && }
{!hideTimeline && ( -
-