├── .github ├── FUNDING.yml └── workflows │ ├── publish-commit.yaml │ ├── publish.yaml │ └── validate.yaml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.MD ├── assets ├── active-page.png ├── active-tab.png ├── boundaries.png ├── error-tab.png ├── errors-tab.gif ├── logs.png ├── remix-dev-tools.png ├── routes-tab.png ├── routes.gif ├── rrd-mascott.png ├── timeline.gif └── tree-view.png ├── biome.json ├── docs ├── .dockerignore ├── .env.example ├── .gitignore ├── .react-router │ └── types │ │ └── app │ │ ├── +types │ │ └── root.ts │ │ └── routes │ │ └── +types │ │ ├── _index.ts │ │ ├── docs.$tag.$slug.ts │ │ ├── docs.$tag._index.ts │ │ ├── docs.$tag.ts │ │ ├── healthcheck.ts │ │ └── updateTheme.ts ├── Dockerfile ├── LICENSE.md ├── README.md ├── app │ ├── components │ │ ├── ClientHint.tsx │ │ ├── FeaturesSection.tsx │ │ ├── ThemeSwitcher.tsx │ │ ├── layout │ │ │ ├── Documentation.tsx │ │ │ ├── Header.tsx │ │ │ └── Sidebar.tsx │ │ ├── plugins │ │ │ ├── Details.tsx │ │ │ ├── Editor.tsx │ │ │ ├── Heading.tsx │ │ │ ├── Info.tsx │ │ │ ├── Snippet.tsx │ │ │ ├── Summary.tsx │ │ │ └── Warn.tsx │ │ └── ui │ │ │ ├── Button.tsx │ │ │ ├── Lamp.tsx │ │ │ ├── MaskContainer.tsx │ │ │ ├── Meteors.tsx │ │ │ ├── Sparkles.tsx │ │ │ ├── background-gradient.tsx │ │ │ ├── infinite-cards.tsx │ │ │ ├── navbar-menu.tsx │ │ │ ├── sticky-scroll-reveral.tsx │ │ │ └── typewritter.tsx │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── hooks │ │ ├── useActionKey.ts │ │ ├── useEventListener.ts │ │ ├── useHints.ts │ │ ├── useIsomorphicLayoutEffect.ts │ │ ├── useMediaQuery.ts │ │ ├── useOnClickOutside.ts │ │ ├── useRequestInfo.ts │ │ ├── useTableOfContents.ts │ │ └── useTheme.ts │ ├── rehype │ │ ├── checkbox.ts │ │ └── role.ts │ ├── remark │ │ ├── highlight.ts │ │ ├── toc.ts │ │ └── utils.ts │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ ├── _index.tsx │ │ ├── docs.$tag.$slug.tsx │ │ ├── docs.$tag._index.tsx │ │ ├── docs.$tag.tsx │ │ ├── healthcheck.tsx │ │ └── updateTheme.ts │ ├── styles │ │ ├── code.css │ │ ├── documentation.css │ │ ├── fonts.css │ │ └── tailwind.css │ ├── types │ │ └── mdx.ts │ └── utils │ │ ├── client │ │ └── nearest-scrollable-container.ts │ │ ├── cn.ts │ │ ├── defatult.ts │ │ └── server │ │ ├── doc.server.ts │ │ ├── mdx.server.ts │ │ └── theme.server.ts ├── env.d.ts ├── fly.toml ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── posts │ ├── main │ │ ├── _index.mdx │ │ ├── configuration │ │ │ ├── client.mdx │ │ │ ├── editor.mdx │ │ │ ├── general.mdx │ │ │ ├── index.md │ │ │ └── server.mdx │ │ ├── features │ │ │ ├── active-page-tab.mdx │ │ │ ├── detach.mdx │ │ │ ├── devtools.mdx │ │ │ ├── errors-tab.mdx │ │ │ ├── index.md │ │ │ ├── network-tab.mdx │ │ │ ├── routes-tab.mdx │ │ │ ├── settings-tab.mdx │ │ │ └── shortcuts.mdx │ │ ├── guides │ │ │ ├── contributing.mdx │ │ │ ├── hydrogen-oxygen.mdx │ │ │ ├── index.md │ │ │ ├── migration.mdx │ │ │ └── plugins.mdx │ │ ├── metadata.json │ │ └── started │ │ │ ├── index.md │ │ │ └── installation.mdx │ └── versions.json ├── public │ ├── AI-Hero.webp │ ├── active-page.png │ ├── active-tab.png │ ├── boundaries.png │ ├── error-tab.png │ ├── errors-tab.gif │ ├── favicon.ico │ ├── fonts │ │ ├── FiraCode │ │ │ ├── FiraCode-Medium.ttf │ │ │ └── FiraCode-Regular.woff2 │ │ ├── Inter │ │ │ ├── Inter-italic.var.woff2 │ │ │ └── Inter-roman.var.woff2 │ │ └── Space │ │ │ └── Space.woff2 │ ├── logs.png │ ├── mask.svg │ ├── rdt.png │ ├── remix.svg │ ├── routes-tab.png │ ├── routes.gif │ ├── timeline.gif │ └── tree-view.png ├── scripts │ └── generate-metadata.ts ├── tailwind.config.js ├── tests │ ├── e2e │ │ └── link-checker.test.ts │ └── setup │ │ ├── global-setup.ts │ │ └── setup-env-test.ts ├── tsconfig.json ├── vite.config.ts └── vitest.config.js ├── knip.json ├── lefthook.yml ├── logo.svg ├── package-lock.json ├── package.json ├── plugins ├── README.md ├── icon-library │ ├── README.md │ ├── icon-library.mp4 │ └── icon-library.tsx └── tailwind-palette │ ├── README.md │ ├── color-palette.mp4 │ └── tailwind-palette.tsx ├── postcss.config.js ├── pull_request_template.md ├── resources └── icons │ ├── accessibility.svg │ ├── activity.svg │ ├── check.svg │ ├── chevron-down.svg │ ├── columns.svg │ ├── copy-slash.svg │ ├── corner-down-right.svg │ ├── git-merge.svg │ ├── layers.svg │ ├── layout.svg │ ├── list.svg │ ├── network.svg │ ├── radio.svg │ ├── root.svg │ ├── send.svg │ ├── settings.svg │ ├── shield.svg │ ├── terminal.svg │ └── x.svg ├── scripts ├── icons.ts └── setup.ts ├── src ├── client.ts ├── client │ ├── components │ │ ├── Accordion.tsx │ │ ├── Breakpoints.tsx │ │ ├── CacheInfo.tsx │ │ ├── Checkbox.tsx │ │ ├── EditorButton.tsx │ │ ├── InfoCard.tsx │ │ ├── Input.tsx │ │ ├── LiveUrls.tsx │ │ ├── Logo.tsx │ │ ├── NewRouteForm.tsx │ │ ├── RouteInfo.tsx │ │ ├── RouteNode.tsx │ │ ├── RouteSegmentInfo.tsx │ │ ├── RouteToggle.tsx │ │ ├── Select.tsx │ │ ├── Stack.tsx │ │ ├── Tag.tsx │ │ ├── Trigger.tsx │ │ ├── icon │ │ │ ├── Icon.tsx │ │ │ └── icons │ │ │ │ ├── icon.svg │ │ │ │ └── types.ts │ │ ├── jsonRenderer.tsx │ │ ├── network-tracer │ │ │ ├── NetworkBar.tsx │ │ │ ├── NetworkPanel.tsx │ │ │ ├── NetworkWaterfall.tsx │ │ │ └── RequestDetails.tsx │ │ └── util.ts │ ├── context │ │ ├── RDTContext.test.tsx │ │ ├── RDTContext.tsx │ │ ├── rdtReducer.test.ts │ │ ├── rdtReducer.ts │ │ ├── requests │ │ │ └── request-context.tsx │ │ ├── terminal │ │ │ └── types.ts │ │ ├── timeline │ │ │ └── types.ts │ │ └── useRDTContext.ts │ ├── embedded-dev-tools.tsx │ ├── hof.test.ts │ ├── hof.ts │ ├── hooks │ │ ├── detached │ │ │ ├── useCheckIfStillDetached.ts │ │ │ ├── useListenToRouteChange.ts │ │ │ ├── useRemoveBody.ts │ │ │ ├── useResetDetachmentCheck.ts │ │ │ └── useSyncStateWhenDetached.ts │ │ ├── useAttachListener.ts │ │ ├── useCountdown.ts │ │ ├── useDebounce.ts │ │ ├── useDevServerConnection.ts │ │ ├── useHorizontalScroll.ts │ │ ├── useOnWindowResize.ts │ │ ├── useOpenElementSource.ts │ │ ├── useReactTreeListeners.ts │ │ ├── useResize.ts │ │ ├── useSetRouteBoundaries.ts │ │ ├── useTabs.ts │ │ └── useTimelineHandler.ts │ ├── init │ │ ├── hydration.ts │ │ └── root.tsx │ ├── layout │ │ ├── ContentPanel.tsx │ │ ├── MainPanel.tsx │ │ └── Tabs.tsx │ ├── react-router-dev-tools.test.tsx │ ├── react-router-dev-tools.tsx │ ├── tabs │ │ ├── ErrorsTab.test.tsx │ │ ├── ErrorsTab.tsx │ │ ├── NetworkTab.tsx │ │ ├── PageTab.test.tsx │ │ ├── PageTab.tsx │ │ ├── RoutesTab.tsx │ │ ├── SettingsTab.tsx │ │ ├── TimelineTab.tsx │ │ └── index.tsx │ └── utils │ │ ├── common.ts │ │ ├── detached.ts │ │ ├── routing.test.ts │ │ ├── routing.ts │ │ ├── sanitize.test.ts │ │ ├── sanitize.ts │ │ ├── storage.test.ts │ │ ├── storage.ts │ │ └── string.ts ├── context.ts ├── context │ ├── extend-context.ts │ └── tracing.ts ├── external │ └── react-json-view │ │ ├── Container.tsx │ │ ├── README.MD │ │ ├── arrow │ │ └── TriangleArrow.tsx │ │ ├── comps │ │ ├── Copied.tsx │ │ ├── KeyValues.tsx │ │ ├── NestedClose.tsx │ │ ├── NestedOpen.tsx │ │ └── Value.tsx │ │ ├── index.tsx │ │ ├── section │ │ ├── Copied.tsx │ │ ├── CountInfo.tsx │ │ ├── CountInfoExtra.tsx │ │ ├── Ellipsis.tsx │ │ └── KeyName.tsx │ │ ├── store.tsx │ │ ├── store │ │ ├── Expands.tsx │ │ ├── Section.tsx │ │ ├── ShowTools.tsx │ │ ├── Symbols.tsx │ │ └── Types.tsx │ │ ├── symbol │ │ ├── Arrow.tsx │ │ ├── BraceLeft.tsx │ │ ├── BraceRight.tsx │ │ ├── BracketsLeft.tsx │ │ ├── BracketsRight.tsx │ │ ├── Colon.tsx │ │ ├── Quote.tsx │ │ ├── ValueQuote.tsx │ │ └── index.tsx │ │ ├── theme │ │ └── custom.tsx │ │ ├── types │ │ ├── Bigint.tsx │ │ ├── Date.tsx │ │ ├── False.tsx │ │ ├── Float.tsx │ │ ├── Int.tsx │ │ ├── Map.tsx │ │ ├── Nan.tsx │ │ ├── Null.tsx │ │ ├── Set.tsx │ │ ├── String.tsx │ │ ├── True.tsx │ │ ├── Undefined.tsx │ │ ├── Url.tsx │ │ └── index.tsx │ │ └── utils │ │ ├── useHighlight.tsx │ │ └── useRender.tsx ├── gradients.css ├── index.ts ├── input.css ├── server.ts ├── server │ ├── config.test.ts │ ├── config.ts │ ├── event-queue.ts │ ├── hof.test.ts │ ├── hof.ts │ ├── logger.ts │ ├── parser.test.ts │ ├── parser.ts │ ├── perf.test.ts │ ├── perf.ts │ ├── utils.test.ts │ └── utils.ts ├── shared │ ├── bigint-util.test.ts │ ├── bigint-util.ts │ ├── request-event.ts │ └── send-event.ts ├── vite-env.d.ts └── vite │ ├── editor.ts │ ├── file.ts │ ├── generators │ ├── action.ts │ ├── clientAction.ts │ ├── clintLoader.ts │ ├── component.ts │ ├── dependencies.ts │ ├── errorBoundary.ts │ ├── handler.ts │ ├── headers.ts │ ├── index.ts │ ├── links.ts │ ├── loader.ts │ ├── meta.ts │ └── revalidate.ts │ ├── node-server.ts │ ├── plugin.tsx │ ├── utils.ts │ └── utils │ ├── babel.ts │ ├── data-functions-augment.test.ts │ ├── data-functions-augment.ts │ ├── inject-client.test.ts │ ├── inject-client.ts │ ├── inject-context.test.ts │ └── inject-context.ts ├── tailwind.config.js ├── test-apps ├── custom-server │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── app.css │ │ ├── root.tsx │ │ ├── routes.ts │ │ ├── routes │ │ │ └── home.tsx │ │ └── welcome │ │ │ ├── logo-dark.svg │ │ │ ├── logo-light.svg │ │ │ └── welcome.tsx │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── react-router.config.ts │ ├── server.js │ ├── server │ │ └── app.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.vite.json │ └── vite.config.ts └── react-router-vite │ ├── .gitignore │ ├── README.md │ ├── app │ ├── actions │ │ └── home.ts │ ├── components │ │ └── Button.tsx │ ├── modules │ │ └── user.server.ts │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ ├── _index.tsx │ │ ├── _layout.added.tsx │ │ ├── _layout.final_test.tsx │ │ ├── _layout.new_route.tsx │ │ ├── _layout.one_more_time.tsx │ │ ├── _layout.tests.$id.edit.new.$test.$wildcard.test │ │ │ └── route.tsx │ │ ├── _layout.tests.$id.edit.new.$test.$wildcard.tsx │ │ ├── _layout.tests.$id.edit.new.$test.tsx │ │ ├── _layout.tests.$id.edit.new.tsx │ │ ├── _layout.tests.$id.edit.tsx │ │ ├── _layout.tests.$id.tsx │ │ ├── _layout.tests.tsx │ │ ├── _layout.tsx │ │ ├── _layout │ │ │ └── test.tsx │ │ ├── correct.tsx │ │ ├── dashboard.tsx │ │ ├── embedded.tsx │ │ ├── epic-test+.tsx │ │ ├── epic │ │ │ ├── __note-editor.server.tsx │ │ │ ├── __note-editor.tsx │ │ │ └── route.tsx │ │ ├── exports.tsx │ │ ├── file.tsx │ │ ├── folder │ │ │ └── route.tsx │ │ ├── home.tsx │ │ ├── login.tsx │ │ ├── logout.tsx │ │ ├── other._index.tsx │ │ ├── other.page.tsx │ │ ├── other.tsx │ │ ├── server-timings.tsx │ │ ├── tester.tsx │ │ └── unexported.tsx │ ├── subroutes.ts │ ├── timing.server.ts │ └── utils │ │ └── example.ts │ ├── env.d.ts │ ├── package.json │ ├── plugins │ └── tailwind-palette.tsx │ ├── public │ └── favicon.ico │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── test ├── console.ts └── setup.tsx ├── tsconfig.json ├── tsup-client.config.ts ├── tsup-context.config.ts ├── tsup-server.config.ts ├── tsup.config.ts └── vitest.workspace.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [AlemTuzlak] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/publish-commit.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 pkg-pr-new 2 | concurrency: 3 | group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - run: corepack enable 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: "npm" 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - run: npx pkg-pr-new publish 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | npm-publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | # Setup .npmrc file to publish to npm 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: "20.x" 16 | registry-url: "https://registry.npmjs.org" 17 | - run: npm ci 18 | - run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | 22 | 23 | deploy-docs: 24 | name: "🚀 Deploy Docs" 25 | runs-on: ubuntu-latest 26 | environment: 27 | name: docs-release 28 | url: ${{ steps.deploy.outputs.app_url }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: forge-42/fly-deploy@v1.0.0-rc.1 32 | id: deploy 33 | env: 34 | FLY_ORG: ${{ vars.FLY_ORG }} 35 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 36 | FLY_REGION: fra 37 | with: 38 | workspace_name: docs 39 | app_name: react-router-devtools-docs-release 40 | use_isolated_workspace: true 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach by Process ID", 6 | "processId": "${command:PickProcess}", 7 | "request": "attach", 8 | "skipFiles": ["/**"], 9 | "type": "node" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Code Forge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.MD: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | In case of a vulnerability please reach out to active maintainers of the project and report it to them. -------------------------------------------------------------------------------- /assets/active-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/active-page.png -------------------------------------------------------------------------------- /assets/active-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/active-tab.png -------------------------------------------------------------------------------- /assets/boundaries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/boundaries.png -------------------------------------------------------------------------------- /assets/error-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/error-tab.png -------------------------------------------------------------------------------- /assets/errors-tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/errors-tab.gif -------------------------------------------------------------------------------- /assets/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/logs.png -------------------------------------------------------------------------------- /assets/remix-dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/remix-dev-tools.png -------------------------------------------------------------------------------- /assets/routes-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/routes-tab.png -------------------------------------------------------------------------------- /assets/routes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/routes.gif -------------------------------------------------------------------------------- /assets/rrd-mascott.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/rrd-mascott.png -------------------------------------------------------------------------------- /assets/timeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/timeline.gif -------------------------------------------------------------------------------- /assets/tree-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/assets/tree-view.png -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "defaultBranch": "main", 7 | "useIgnoreFile": true 8 | }, 9 | "formatter": { 10 | "enabled": true, 11 | "formatWithErrors": false, 12 | "indentStyle": "tab", 13 | "lineEnding": "lf", 14 | "lineWidth": 120 15 | }, 16 | "files": { 17 | "ignore": ["test-apps/**", "docs/**", "./src/external/**", "./plugins/**", "./src/input.css", "./src/gradients.css"] 18 | }, 19 | "organizeImports": { 20 | "enabled": true 21 | }, 22 | "linter": { 23 | "enabled": true, 24 | "rules": { 25 | "recommended": true, 26 | "suspicious": { 27 | "recommended": true, 28 | "noExplicitAny": "off", 29 | "noConsole": { 30 | "level": "error", 31 | "options": { 32 | "allow": ["assert", "error", "info", "warn"] 33 | } 34 | } 35 | }, 36 | "style": { 37 | "recommended": true 38 | }, 39 | "complexity": { 40 | "recommended": true 41 | }, 42 | "security": { 43 | "recommended": true 44 | }, 45 | "performance": { 46 | "recommended": true 47 | }, 48 | "correctness": { 49 | "recommended": true 50 | }, 51 | "a11y": { 52 | "recommended": true 53 | }, 54 | "nursery": { 55 | "recommended": true 56 | } 57 | } 58 | }, 59 | "javascript": { 60 | "formatter": { 61 | "semicolons": "asNeeded", 62 | "trailingCommas": "es5" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | 4 | /.cache 5 | /.github 6 | /build 7 | .env 8 | 9 | ./test-results 10 | ./coverage 11 | ./playwright-report/** 12 | ./tests 13 | ./scripts 14 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN="ghp_your-github-token" 2 | GITHUB_OWNER="your-username" 3 | GITHUB_REPO="your-repo" 4 | APP_ROOT_PATH="/path/to/your/app" # Optional. Default is `process.cwd()` 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | 7 | ./test-results 8 | ./coverage 9 | ./playwright-report/** 10 | 11 | ./public/entry.worker.js -------------------------------------------------------------------------------- /docs/.react-router/types/app/+types/root.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // root.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | 7 | 8 | type Module = typeof import("../root") 9 | 10 | export type Info = { 11 | parents: [], 12 | id: "root" 13 | file: "root.tsx" 14 | path: "" 15 | params: {} 16 | module: Module 17 | loaderData: T.CreateLoaderData 18 | actionData: T.CreateActionData 19 | } 20 | 21 | export namespace Route { 22 | export type LinkDescriptors = T.LinkDescriptors 23 | export type LinksFunction = () => LinkDescriptors 24 | 25 | export type MetaArgs = T.CreateMetaArgs 26 | export type MetaDescriptors = T.MetaDescriptors 27 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 28 | 29 | export type LoaderArgs = T.CreateServerLoaderArgs 30 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 31 | export type ActionArgs = T.CreateServerActionArgs 32 | export type ClientActionArgs = T.CreateClientActionArgs 33 | 34 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 35 | export type ComponentProps = T.CreateComponentProps 36 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 37 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/_index.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/_index.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | 8 | type Module = typeof import("../_index") 9 | 10 | export type Info = { 11 | parents: [Parent0], 12 | id: "routes/_index" 13 | file: "routes/_index.tsx" 14 | path: "undefined" 15 | params: {} 16 | module: Module 17 | loaderData: T.CreateLoaderData 18 | actionData: T.CreateActionData 19 | } 20 | 21 | export namespace Route { 22 | export type LinkDescriptors = T.LinkDescriptors 23 | export type LinksFunction = () => LinkDescriptors 24 | 25 | export type MetaArgs = T.CreateMetaArgs 26 | export type MetaDescriptors = T.MetaDescriptors 27 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 28 | 29 | export type LoaderArgs = T.CreateServerLoaderArgs 30 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 31 | export type ActionArgs = T.CreateServerActionArgs 32 | export type ClientActionArgs = T.CreateClientActionArgs 33 | 34 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 35 | export type ComponentProps = T.CreateComponentProps 36 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 37 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/docs.$tag.$slug.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/docs.$tag.$slug.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | import type { Info as Parent1 } from "./docs.$tag" 8 | 9 | type Module = typeof import("../docs.$tag.$slug") 10 | 11 | export type Info = { 12 | parents: [Parent0, Parent1], 13 | id: "routes/docs.$tag.$slug" 14 | file: "routes/docs.$tag.$slug.tsx" 15 | path: ":slug" 16 | params: {"tag": string; "slug": string} 17 | module: Module 18 | loaderData: T.CreateLoaderData 19 | actionData: T.CreateActionData 20 | } 21 | 22 | export namespace Route { 23 | export type LinkDescriptors = T.LinkDescriptors 24 | export type LinksFunction = () => LinkDescriptors 25 | 26 | export type MetaArgs = T.CreateMetaArgs 27 | export type MetaDescriptors = T.MetaDescriptors 28 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 29 | 30 | export type LoaderArgs = T.CreateServerLoaderArgs 31 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 32 | export type ActionArgs = T.CreateServerActionArgs 33 | export type ClientActionArgs = T.CreateClientActionArgs 34 | 35 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 36 | export type ComponentProps = T.CreateComponentProps 37 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 38 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/docs.$tag._index.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/docs.$tag._index.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | import type { Info as Parent1 } from "./docs.$tag" 8 | 9 | type Module = typeof import("../docs.$tag._index") 10 | 11 | export type Info = { 12 | parents: [Parent0, Parent1], 13 | id: "routes/docs.$tag._index" 14 | file: "routes/docs.$tag._index.tsx" 15 | path: "undefined" 16 | params: {"tag": string} 17 | module: Module 18 | loaderData: T.CreateLoaderData 19 | actionData: T.CreateActionData 20 | } 21 | 22 | export namespace Route { 23 | export type LinkDescriptors = T.LinkDescriptors 24 | export type LinksFunction = () => LinkDescriptors 25 | 26 | export type MetaArgs = T.CreateMetaArgs 27 | export type MetaDescriptors = T.MetaDescriptors 28 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 29 | 30 | export type LoaderArgs = T.CreateServerLoaderArgs 31 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 32 | export type ActionArgs = T.CreateServerActionArgs 33 | export type ClientActionArgs = T.CreateClientActionArgs 34 | 35 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 36 | export type ComponentProps = T.CreateComponentProps 37 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 38 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/docs.$tag.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/docs.$tag.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | 8 | type Module = typeof import("../docs.$tag") 9 | 10 | export type Info = { 11 | parents: [Parent0], 12 | id: "routes/docs.$tag" 13 | file: "routes/docs.$tag.tsx" 14 | path: "docs/:tag" 15 | params: {"tag": string} 16 | module: Module 17 | loaderData: T.CreateLoaderData 18 | actionData: T.CreateActionData 19 | } 20 | 21 | export namespace Route { 22 | export type LinkDescriptors = T.LinkDescriptors 23 | export type LinksFunction = () => LinkDescriptors 24 | 25 | export type MetaArgs = T.CreateMetaArgs 26 | export type MetaDescriptors = T.MetaDescriptors 27 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 28 | 29 | export type LoaderArgs = T.CreateServerLoaderArgs 30 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 31 | export type ActionArgs = T.CreateServerActionArgs 32 | export type ClientActionArgs = T.CreateClientActionArgs 33 | 34 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 35 | export type ComponentProps = T.CreateComponentProps 36 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 37 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/healthcheck.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/healthcheck.tsx 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | 8 | type Module = typeof import("../healthcheck") 9 | 10 | export type Info = { 11 | parents: [Parent0], 12 | id: "routes/healthcheck" 13 | file: "routes/healthcheck.tsx" 14 | path: "healthcheck" 15 | params: {} 16 | module: Module 17 | loaderData: T.CreateLoaderData 18 | actionData: T.CreateActionData 19 | } 20 | 21 | export namespace Route { 22 | export type LinkDescriptors = T.LinkDescriptors 23 | export type LinksFunction = () => LinkDescriptors 24 | 25 | export type MetaArgs = T.CreateMetaArgs 26 | export type MetaDescriptors = T.MetaDescriptors 27 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 28 | 29 | export type LoaderArgs = T.CreateServerLoaderArgs 30 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 31 | export type ActionArgs = T.CreateServerActionArgs 32 | export type ClientActionArgs = T.CreateClientActionArgs 33 | 34 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 35 | export type ComponentProps = T.CreateComponentProps 36 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 37 | } -------------------------------------------------------------------------------- /docs/.react-router/types/app/routes/+types/updateTheme.ts: -------------------------------------------------------------------------------- 1 | // React Router generated types for route: 2 | // routes/updateTheme.ts 3 | 4 | import type * as T from "react-router/route-module" 5 | 6 | import type { Info as Parent0 } from "../../+types/root" 7 | 8 | type Module = typeof import("../updateTheme") 9 | 10 | export type Info = { 11 | parents: [Parent0], 12 | id: "routes/updateTheme" 13 | file: "routes/updateTheme.ts" 14 | path: "updateTheme" 15 | params: {} 16 | module: Module 17 | loaderData: T.CreateLoaderData 18 | actionData: T.CreateActionData 19 | } 20 | 21 | export namespace Route { 22 | export type LinkDescriptors = T.LinkDescriptors 23 | export type LinksFunction = () => LinkDescriptors 24 | 25 | export type MetaArgs = T.CreateMetaArgs 26 | export type MetaDescriptors = T.MetaDescriptors 27 | export type MetaFunction = (args: MetaArgs) => MetaDescriptors 28 | 29 | export type LoaderArgs = T.CreateServerLoaderArgs 30 | export type ClientLoaderArgs = T.CreateClientLoaderArgs 31 | export type ActionArgs = T.CreateServerActionArgs 32 | export type ClientActionArgs = T.CreateClientActionArgs 33 | 34 | export type HydrateFallbackProps = T.CreateHydrateFallbackProps 35 | export type ComponentProps = T.CreateComponentProps 36 | export type ErrorBoundaryProps = T.CreateErrorBoundaryProps 37 | } -------------------------------------------------------------------------------- /docs/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.4 2 | 3 | # Adjust NODE_VERSION as desired 4 | ARG NODE_VERSION=22 5 | FROM node:${NODE_VERSION}-slim AS base 6 | 7 | # React router app lives here 8 | WORKDIR /app 9 | 10 | # Set production environment 11 | ENV NODE_ENV="production" 12 | 13 | 14 | # Throw-away build stage to reduce size of final image 15 | FROM base as build 16 | 17 | # Install packages needed to build node modules 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 20 | 21 | # Install node modules 22 | COPY --link package-lock.json package.json ./ 23 | RUN npm ci --include=dev 24 | 25 | # Copy application code 26 | COPY --link . . 27 | 28 | # Build application 29 | RUN npm run build 30 | 31 | # Remove development dependencies 32 | RUN npm prune --omit=dev 33 | 34 | 35 | # Final stage for app image 36 | FROM base 37 | 38 | # Copy built application 39 | COPY --from=build / / 40 | 41 | # Start the server by default, this can be overwritten at runtime 42 | EXPOSE 3000 43 | CMD [ "node_modules/.bin/react-router-serve", "./build/server/index.js" ] 44 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) ShafSpecs 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/app/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { MoonIcon, SunIcon } from 'lucide-react' 3 | import { useRef } from 'react' 4 | import { useFetcher } from 'react-router' 5 | 6 | import { useTheme } from '~/hooks/useTheme' 7 | 8 | export const PWAThemeSwitcher = () => { 9 | const fetcher = useFetcher() 10 | const theme = useTheme() 11 | const ref = useRef(null) 12 | const themeSwitcher = () => { 13 | if (theme === 'light') { 14 | fetcher.submit( 15 | { theme: 'dark' }, 16 | { method: 'post', action: '/updateTheme' } 17 | ) 18 | } else if (theme === 'dark') { 19 | fetcher.submit( 20 | { theme: 'light' }, 21 | { method: 'post', action: '/updateTheme' } 22 | ) 23 | } 24 | } 25 | const TOGGLE_THEME = () => { 26 | if (!document.startViewTransition) themeSwitcher() 27 | 28 | document.startViewTransition(themeSwitcher) 29 | } 30 | return ( 31 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /docs/app/components/plugins/Details.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode, JSX } from 'react' 2 | 3 | // eslint-disable-next-line react/display-name 4 | export default ({ 5 | children, 6 | title, 7 | }: { 8 | title: string 9 | children: ReactNode | JSX.Element 10 | }) => { 11 | return ( 12 |
13 | 14 | {title} 15 | 16 | {children} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /docs/app/components/plugins/Summary.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode, JSX } from 'react' 2 | 3 | export const PWASummary = ({ 4 | children, 5 | title, 6 | }: { 7 | title: string 8 | children: ReactNode | JSX.Element 9 | }) => { 10 | return ( 11 |
12 | 13 | {title} 14 | 15 | {children} 16 |
17 | ) 18 | } 19 | 20 | const Summary = PWASummary 21 | export default Summary 22 | -------------------------------------------------------------------------------- /docs/app/components/ui/Meteors.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '~/utils/cn' 2 | export const Meteors = ({ 3 | number, 4 | className, 5 | }: { 6 | number?: number 7 | className?: string 8 | }) => { 9 | const meteors = new Array(number || 20).fill(true) 10 | 11 | return meteors.map((el, idx) => ( 12 | 26 | )) 27 | } 28 | -------------------------------------------------------------------------------- /docs/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { HydratedRouter } from 'react-router/dom' 2 | import { startTransition, StrictMode } from 'react' 3 | import { hydrateRoot } from 'react-dom/client' 4 | import { configureGlobalCache } from 'remix-client-cache' 5 | 6 | configureGlobalCache(() => localStorage) 7 | 8 | startTransition(() => { 9 | hydrateRoot( 10 | document, 11 | 12 | 13 | 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /docs/app/hooks/useActionKey.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const ACTION_KEY_DEFAULT = ['Ctrl ', 'Control'] 4 | const ACTION_KEY_APPLE = ['⌘', 'Command'] 5 | 6 | export function useActionKey() { 7 | const [actionKey, setActionKey] = useState(['', '']) 8 | 9 | useEffect(() => { 10 | if (typeof navigator !== 'undefined') { 11 | if (/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) { 12 | // update to `navigator.userAgentData.platform` when it becomes accepted 13 | setActionKey(ACTION_KEY_APPLE) 14 | } else { 15 | setActionKey(ACTION_KEY_DEFAULT) 16 | } 17 | } 18 | }, []) 19 | 20 | return actionKey 21 | } 22 | -------------------------------------------------------------------------------- /docs/app/hooks/useHints.ts: -------------------------------------------------------------------------------- 1 | import { useRequestInfo } from './useRequestInfo' 2 | 3 | export function useHints() { 4 | const requestInfo = useRequestInfo() 5 | return requestInfo.hints 6 | } 7 | -------------------------------------------------------------------------------- /docs/app/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | export const useIsomorphicLayoutEffect = 4 | typeof window !== 'undefined' ? useLayoutEffect : useEffect 5 | -------------------------------------------------------------------------------- /docs/app/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function useMediaQuery(query: string): boolean { 4 | const getMatches = (query: string): boolean => { 5 | // Prevents SSR issues 6 | if (typeof window !== 'undefined') { 7 | return window.matchMedia(query).matches 8 | } 9 | return false 10 | } 11 | 12 | const [matches, setMatches] = useState(getMatches(query)) 13 | 14 | function handleChange() { 15 | setMatches(getMatches(query)) 16 | } 17 | 18 | useEffect(() => { 19 | const matchMedia = window.matchMedia(query) 20 | 21 | // Triggered at the first client-side load and if query changes 22 | handleChange() 23 | 24 | // Listen matchMedia 25 | if (matchMedia.addListener) { 26 | matchMedia.addListener(handleChange) 27 | } else { 28 | matchMedia.addEventListener('change', handleChange) 29 | } 30 | 31 | return () => { 32 | if (matchMedia.removeListener) { 33 | matchMedia.removeListener(handleChange) 34 | } else { 35 | matchMedia.removeEventListener('change', handleChange) 36 | } 37 | } 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, [query]) 40 | 41 | return matches 42 | } 43 | -------------------------------------------------------------------------------- /docs/app/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from 'react' 2 | 3 | import { useEventListener } from './useEventListener' 4 | 5 | type Handler = (event: MouseEvent) => void 6 | 7 | export function useOnClickOutside( 8 | ref: RefObject, 9 | handler: Handler, 10 | mouseEvent: 'mousedown' | 'mouseup' = 'mousedown' 11 | ): void { 12 | useEventListener(mouseEvent, event => { 13 | const el = ref?.current 14 | 15 | // Do nothing if clicking ref's element or descendent elements 16 | if (!el || el.contains(event.target as Node)) { 17 | return 18 | } 19 | 20 | handler(event) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /docs/app/hooks/useRequestInfo.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '@epic-web/invariant' 2 | import { useRouteLoaderData } from 'react-router' 3 | 4 | import { type loader as rootLoader } from '~/root.tsx' 5 | 6 | /** 7 | * @returns the request info from the root loader 8 | */ 9 | export function useRequestInfo() { 10 | const data = useRouteLoaderData('root') 11 | invariant(data?.requestInfo, 'No requestInfo found in root loader') 12 | 13 | return data.requestInfo 14 | } 15 | -------------------------------------------------------------------------------- /docs/app/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useFetchers } from 'react-router' 2 | import { z } from 'zod' 3 | 4 | import { useHints } from './useHints' 5 | import { useRequestInfo } from './useRequestInfo' 6 | 7 | const THEME_COOKIE_KEY = 'theme' 8 | type Theme = 'light' | 'dark' | undefined 9 | 10 | export const ThemeFormSchema = z.object({ 11 | theme: z.enum(['system', 'light', 'dark']), 12 | redirectTo: z.string().optional(), 13 | }) 14 | 15 | /** 16 | * Returns the user's theme preference, or the Client Hint theme, 17 | * if the user has not set a preference. 18 | */ 19 | export function useTheme() { 20 | const hints = useHints() 21 | const requestInfo = useRequestInfo() 22 | const optimisticMode = useOptimisticThemeMode() 23 | 24 | if (optimisticMode) { 25 | return optimisticMode === 'system' ? hints.theme : optimisticMode 26 | } 27 | 28 | return requestInfo.userPrefs.theme ?? hints.theme 29 | } 30 | 31 | /** 32 | * If the user's changing their theme mode preference, 33 | * this will return the value it's being changed to. 34 | */ 35 | function useOptimisticThemeMode() { 36 | const fetchers = useFetchers() 37 | const themeFetcher = fetchers.find( 38 | f => f.formAction?.startsWith('/updateTheme') 39 | ) 40 | 41 | if (themeFetcher && themeFetcher.formData) { 42 | const formData = Object.fromEntries(themeFetcher.formData) 43 | const { theme } = ThemeFormSchema.parse(formData) 44 | 45 | return theme 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/app/rehype/checkbox.ts: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | 3 | export default () => { 4 | return (tree: any) => { 5 | visit(tree, 'element', element => { 6 | if ( 7 | element.tagName === 'input' && 8 | element.properties.type === 'checkbox' 9 | ) { 10 | element.properties.className = 'hidden' 11 | } 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/app/rehype/role.ts: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | 3 | export default () => { 4 | return (tree: any) => { 5 | visit(tree, 'element', element => { 6 | if (['ol', 'ul'].includes(element.tagName)) { 7 | element.properties.role = 'list' 8 | } 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/app/root.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ClientHints, getHints } from './components/ClientHint' 3 | import { useTheme } from './hooks/useTheme' 4 | import { getTheme } from './utils/server/theme.server' 5 | import { getVersions } from './utils/server/doc.server' 6 | 7 | import './styles/code.css' 8 | import './styles/documentation.css' 9 | import './styles/fonts.css' 10 | import './styles/tailwind.css' 11 | import { Links, LoaderFunctionArgs, Meta, MetaFunction, Outlet, Scripts, ScrollRestoration } from 'react-router' 12 | 13 | export const loader = async ({ request }: LoaderFunctionArgs) => { 14 | const versions = (await getVersions()) ?? [] 15 | 16 | return ({ 17 | requestInfo: { 18 | hints: getHints(request), 19 | userPrefs: { theme: getTheme(request) }, 20 | }, 21 | versions, 22 | }) 23 | } 24 | export const meta: MetaFunction = () => { 25 | return [ 26 | { 27 | property: 'og:site_name', 28 | content: 'React Router Devtools', 29 | }, 30 | { 31 | property: 'og:title', 32 | content: 'React Router Devtools Documentation', 33 | }, 34 | { 35 | property: 'og:image', 36 | content: '/rdt.png', 37 | }, 38 | ] 39 | } 40 | 41 | export default function Document() { 42 | const theme = useTheme() 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /docs/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { flatRoutes } from "@react-router/fs-routes" 2 | 3 | export default flatRoutes() 4 | -------------------------------------------------------------------------------- /docs/app/routes/docs.$tag.$slug.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoaderFunctionArgs } from 'react-router' 3 | import { Documentation } from '~/components/layout/Documentation' 4 | import { 5 | getPostContent, 6 | getPreviousAndNextRoutes, 7 | } from '~/utils/server/doc.server' 8 | import { mdxToHtml } from '~/utils/server/mdx.server' 9 | 10 | export const loader = async ({ params }: LoaderFunctionArgs) => { 11 | const tag = params.tag ?? 'main' 12 | const slug = params.slug as string 13 | 14 | const postContent = (await getPostContent(tag, slug)) ?? '' // handle null cases later 15 | const { code, frontmatter } = await mdxToHtml(postContent) 16 | const [prev, next] = await getPreviousAndNextRoutes(tag, slug) 17 | 18 | return ({ 19 | frontmatter, 20 | code, 21 | next, 22 | prev, 23 | tag, 24 | }) 25 | } 26 | 27 | export default function DocRoute() { 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /docs/app/routes/docs.$tag._index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoaderFunctionArgs, redirect } from 'react-router' 3 | 4 | import { Documentation } from '~/components/layout/Documentation' 5 | import { 6 | getFirstPost, 7 | getPostContent, 8 | redirectToFirstPost, 9 | tagHasIndex, 10 | } from '~/utils/server/doc.server' 11 | import { mdxToHtml } from '~/utils/server/mdx.server' 12 | 13 | export const loader = async ({ params }: LoaderFunctionArgs) => { 14 | const tag = params.tag ?? 'main' 15 | const hasIndex = await tagHasIndex(tag) 16 | 17 | if (!hasIndex) { 18 | throw redirect(`/docs/${tag}/${await redirectToFirstPost(tag)}`) 19 | } 20 | 21 | const postContent = (await getPostContent(tag, '/')) ?? '' // handle null cases later 22 | 23 | const { code, frontmatter } = await mdxToHtml(postContent) 24 | const next = await getFirstPost(tag) 25 | 26 | return ({ 27 | frontmatter, 28 | code, 29 | next, 30 | prev: null, 31 | tag, 32 | }) 33 | } 34 | 35 | export default function TagRoute() { 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /docs/app/routes/docs.$tag.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type { ClientLoaderFunctionArgs, LoaderFunctionArgs } from 'react-router' 3 | import { Outlet } from 'react-router' 4 | import { cacheClientLoader, useCachedLoaderData } from 'remix-client-cache' 5 | 6 | import { Sidebar } from '~/components/layout/Sidebar' 7 | import { getParsedMetadata } from '~/utils/server/doc.server' 8 | import type { MetadataType } from '~/utils/server/doc.server' 9 | 10 | export const loader = async ({ params }: LoaderFunctionArgs) => { 11 | let metadata: MetadataType = { 12 | paths: {}, 13 | hasIndex: false, 14 | sections: [], 15 | meta: {}, 16 | } // default values 17 | 18 | if (params.tag) { 19 | metadata = (await getParsedMetadata(params.tag)) ?? metadata 20 | } 21 | 22 | return ({ 23 | metadata, 24 | tag: params.tag, 25 | }) 26 | } 27 | 28 | export const clientLoader = async (args: ClientLoaderFunctionArgs) => 29 | cacheClientLoader(args) 30 | 31 | clientLoader.hydrate = true 32 | 33 | export default function TagRoute() { 34 | const { metadata } = useCachedLoaderData<{ 35 | metadata: MetadataType 36 | }>() 37 | 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /docs/app/routes/healthcheck.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from '@react-router/node' 2 | 3 | export async function loader({ request }: LoaderFunctionArgs) { 4 | try { 5 | await new Promise(resolve => setTimeout(resolve, 1000)) 6 | 7 | console.log(request.url, 'healthcheck ✅') 8 | 9 | return new Response('OK') 10 | } catch (error: unknown) { 11 | console.error(request.url, 'healthcheck ❌', { error }) 12 | return new Response('ERROR', { status: 500 }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/app/routes/updateTheme.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { ActionFunctionArgs, data, redirect } from 'react-router' 4 | import { ThemeFormSchema } from '~/hooks/useTheme' 5 | import { setTheme } from '~/utils/server/theme.server' 6 | 7 | export async function action({ request }: ActionFunctionArgs) { 8 | const formData = Object.fromEntries(await request.formData()) 9 | const { redirectTo, theme } = ThemeFormSchema.parse(formData) 10 | 11 | const responseInit = { 12 | headers: { 'Set-Cookie': setTheme(theme) }, 13 | } 14 | 15 | if (redirectTo) { 16 | return redirect(redirectTo, responseInit) 17 | } 18 | 19 | return data({ success: true }, responseInit) 20 | } 21 | -------------------------------------------------------------------------------- /docs/app/styles/fonts.css: -------------------------------------------------------------------------------- 1 | /* Fonts Stylesheet */ 2 | 3 | /* -- Inter Font -- */ 4 | @font-face { 5 | font-family: "Inter var"; 6 | font-weight: 100 900; 7 | font-display: block; 8 | font-style: normal; 9 | font-named-instance: "Regular"; 10 | src: url("/fonts/Inter/Inter-roman.var.woff2") format("woff2"); 11 | } 12 | 13 | @font-face { 14 | font-family: "Inter var"; 15 | font-weight: 100 900; 16 | font-display: block; 17 | font-style: italic; 18 | font-named-instance: "Italic"; 19 | src: url("/fonts/Inter/Inter-italic.var.woff2") format("woff2"); 20 | } 21 | 22 | /* -- FiraCode Font -- */ 23 | @font-face { 24 | font-family: "Fira Code"; 25 | font-weight: 400; 26 | font-display: block; 27 | font-style: normal; 28 | font-named-instance: "Regular"; 29 | src: url("/fonts/FiraCode/FiraCode-Regular.woff2") format("woff2"); 30 | } 31 | 32 | @font-face { 33 | font-family: "Fira Code"; 34 | font-weight: 500; 35 | font-display: block; 36 | font-style: normal; 37 | font-named-instance: "Medium"; 38 | src: url("/fonts/FiraCode/FiraCode-Medium.ttf"); 39 | } 40 | 41 | 42 | /* -- SpaceTime Font -- */ 43 | @font-face { 44 | font-family: "Space"; 45 | font-weight: 400; 46 | font-style: normal; 47 | src: url("/fonts/Space/Space.woff2") format("woff2"); 48 | } 49 | 50 | /* Feel free to add more fonts here! */ 51 | -------------------------------------------------------------------------------- /docs/app/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background-color: #fff; 7 | } 8 | 9 | :root.dark { 10 | --background-color: #020617; 11 | } 12 | 13 | html { 14 | font-family: 15 | 'Inter var', 16 | ui-sans-serif, 17 | system-ui, 18 | -apple-system, 19 | BlinkMacSystemFont, 20 | Segoe UI, 21 | Roboto, 22 | Helvetica Neue; 23 | font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; 24 | } 25 | 26 | body { 27 | background-color: var(--background-color); 28 | } 29 | 30 | .dark :not(.placeholder-index) { 31 | color-scheme: dark; 32 | } 33 | -------------------------------------------------------------------------------- /docs/app/types/mdx.ts: -------------------------------------------------------------------------------- 1 | export type FrontMatterType = { 2 | title: string 3 | alternateTitle?: string 4 | order?: number 5 | description?: string 6 | toc?: boolean 7 | hidden?: boolean 8 | spacer?: boolean 9 | } 10 | -------------------------------------------------------------------------------- /docs/app/utils/client/nearest-scrollable-container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Find the nearst scrollable ancestor (or self if scrollable) 3 | * 4 | * Code adapted and simplified from the smoothscroll polyfill 5 | * @param {Element} el 6 | */ 7 | export function nearestScrollableContainer(el: Element) { 8 | /** 9 | * indicates if an element can be scrolled 10 | * @param {Node} el 11 | */ 12 | function isScrollable(el: Element) { 13 | const style = window.getComputedStyle(el) 14 | const overflowX = style.overflowX 15 | const overflowY = style.overflowY 16 | const canScrollY = el.clientHeight < el.scrollHeight 17 | const canScrollX = el.clientWidth < el.scrollWidth 18 | 19 | const isScrollableY = 20 | canScrollY && (overflowY === 'auto' || overflowY === 'scroll') 21 | const isScrollableX = 22 | canScrollX && (overflowX === 'auto' || overflowX === 'scroll') 23 | 24 | return isScrollableY || isScrollableX 25 | } 26 | 27 | while (el !== document.body && isScrollable(el) === false) { 28 | // @ts-ignore 29 | el = el.parentNode || el.host 30 | } 31 | 32 | return el 33 | } 34 | -------------------------------------------------------------------------------- /docs/app/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /docs/app/utils/defatult.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TAG = 'main' 2 | -------------------------------------------------------------------------------- /docs/app/utils/server/theme.server.ts: -------------------------------------------------------------------------------- 1 | import { serialize, parse } from 'cookie' 2 | 3 | const cookieName = 'en_theme' 4 | type Theme = 'light' | 'dark' 5 | 6 | export function getTheme(request: Request): Theme | null { 7 | const cookieHeader = request.headers.get('cookie') 8 | const parsed = cookieHeader ? parse(cookieHeader)[cookieName] : 'light' 9 | if (parsed === 'light' || parsed === 'dark') return parsed 10 | return null 11 | } 12 | 13 | export function setTheme(theme: Theme | 'system') { 14 | if (theme === 'system') { 15 | return serialize(cookieName, '', { path: '/', maxAge: -1 }) 16 | } else { 17 | return serialize(cookieName, theme, { 18 | path: '/', 19 | maxAge: 31_536_000, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for react-router-devtools on 2024-03-09T20:07:33+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'react-router-devtools' 7 | primary_region = 'lhr' 8 | 9 | [build] 10 | dockerfile = "./Dockerfile" 11 | 12 | [http_service] 13 | internal_port = 3000 14 | force_https = true 15 | auto_stop_machines = "suspend" 16 | auto_start_machines = true 17 | min_machines_running = 0 18 | processes = ['app'] 19 | 20 | [[vm]] 21 | memory = '512mb' 22 | cpu_kind = 'shared' 23 | cpus = 1 24 | -------------------------------------------------------------------------------- /docs/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | const PORT = process.env.PORT || '3000' 5 | 6 | export default defineConfig({ 7 | testDir: './tests/e2e', 8 | timeout: 30 * 1000, 9 | expect: { 10 | timeout: 5 * 1000, 11 | }, 12 | fullyParallel: true, 13 | forbidOnly: !!process.env.CI, 14 | retries: process.env.CI ? 2 : 0, 15 | workers: process.env.CI ? 1 : undefined, 16 | reporter: 'html', 17 | use: { 18 | baseURL: `http://localhost:${PORT}/`, 19 | trace: 'on-first-retry', 20 | }, 21 | 22 | projects: [ 23 | { 24 | name: 'chromium', 25 | use: { 26 | ...devices['Desktop Chrome'], 27 | }, 28 | }, 29 | ], 30 | 31 | webServer: { 32 | command: process.env.CI ? 'npm run start' : 'npm run dev', 33 | port: Number(PORT), 34 | reuseExistingServer: !process.env.CI, 35 | stdout: 'pipe', 36 | stderr: 'pipe', 37 | env: { 38 | PORT, 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /docs/posts/main/_index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | hidden: true 4 | toc: false 5 | --- 6 | 7 | import Info from './info.tsx' 8 | 9 | This documentation covers everything you need to know to get started with `react-router-devtools`. 10 | 11 | ## Prerequisites 12 | 13 | - React Router version **7.0** or higher. 14 | - Your project needs to run on **ESM**. If you are using CommonJS, you will need to convert your project to ESM. 15 | 16 | ## Why ESM? 17 | 18 | In order to use the full feature-set of **Vite** the project has to run on ESM. This is because Vite exposes a websocket 19 | that **react-router-devtools** uses to communicate with the server. This websocket is **only** available in ESM projects 20 | because it's exposed by `import.meta` which is only available in ESM. 21 | 22 | To avoid creating user confusion and giving you a subpar experience, we have decided to only support ESM projects running on Vite. 23 | 24 | 25 | ## Why use `react-router-devtools`? 26 | 27 | `react-router-devtools` is a set of tools that help you to develop your React Router application. 28 | 29 | They help you, but are not limited to, to do the following things: 30 | - **Loader data display** - You can see the data that is being loaded by your loaders. 31 | - **Route display** - You can see the routes that are being used by your application in list/tree format. 32 | - **Error tracking** - You can see invalid HTML rendered on your page and where it's coming from. 33 | - **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. 34 | - **Server logs** - You can see the logs of your server in the browser. 35 | - **Route boundaries** - You can see the route boundaries by hovering over elements. 36 | -------------------------------------------------------------------------------- /docs/posts/main/configuration/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | order: 3 4 | --- -------------------------------------------------------------------------------- /docs/posts/main/features/detach.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Detached mode" 3 | alternateTitle: "Detached mode" 4 | description: "How you can detach your panel into a new window" 5 | --- 6 | 7 | There is a special button on the bottom left side of the panel above the X button. 8 | When you click on it, the panel will detach and open in a new window. 9 | 10 | The detached window will keep in sync with the main panel and will show the same content. 11 | The logs on the server will happen twice, once for the main panel and once for the detached window. 12 | 13 | When you close the detached window, or the main panel, the other one will be terminated and closed. 14 | In case the detached mode hangs for some reason, you can always return it to the main site by 15 | clicking the trigger while in detached mode. -------------------------------------------------------------------------------- /docs/posts/main/features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | order: 2 4 | --- -------------------------------------------------------------------------------- /docs/posts/main/features/shortcuts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Keyboard Shortcuts" 3 | description: "Detailed overview of all keyboard shortcuts in React Router Devtools" 4 | --- 5 | 6 | ## Go To Source 7 | 8 | **Shift + Right Click** 9 | 10 | 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 11 | while holding down shift. This will open the source code of the component in your code editor. 12 | 13 | ## Opening/closing the DevTools 14 | 15 | **Shift + A** 16 | 17 | When you are in the browser and you want to open the React Router Devtools, you can press `Shift + A`. 18 | This will open the DevTools, if you're already in the DevTools, it will close it. 19 | 20 | While in the DevTools, you can also use `Esc` to close them. 21 | 22 | From version 4.2.0 is fully configurable and you can change the shortcut in the settings. 23 | 24 | We use [react-hotkeys-hook](https://www.npmjs.com/package/react-hotkeys-hook) to handle the keyboard shortcuts under the hood. 25 | You can adapt to their API to add your own shortcuts. 26 | 27 | Check out the settings tab for details -------------------------------------------------------------------------------- /docs/posts/main/guides/contributing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Contributing to React Router Devtools" 3 | description: "Contributions to React Router Devtools are welcome! To contribute, please follow these guidelines." 4 | --- 5 | 6 | ## Contributing 7 | 8 | Contributions to React Router Devtools are welcome! To contribute, please follow these guidelines: 9 | 10 | 1. Fork the repository and clone it locally. 11 | 2. Create a new branch for your feature or bug fix. 12 | 3. Run `npm install` 13 | 4. Run `npm run dev` to start the development server with a vanilla React Router app setup. 14 | 5. Implement your changes, adhering to the existing code style and best practices. 15 | 5. Please add tests for any new features or bug fixes. 16 | 6. Commit and push your changes to your forked repository. 17 | 7. Open a pull request, providing a clear description of your changes and their purpose. 18 | -------------------------------------------------------------------------------- /docs/posts/main/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | order: 4 4 | --- -------------------------------------------------------------------------------- /docs/posts/main/guides/migration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Migration guide" 3 | description: "Migration guide from remix-development-tools" 4 | --- 5 | 6 | import Info from "./info.tsx"; 7 | 8 | 9 | ### vite.config.ts 10 | 11 | If you're migrating your `remix-development-tools` from v4.x to react-router v7 and you were already running it as 12 | a Vite plugin here is all you need to do: 13 | 14 | ```diff 15 | import { defineConfig } from 'vite'; 16 | - import { vitePlugin as remix } from '@remix-run/dev'; 17 | + import { reactRouter } from '@react-router/dev/vite'; 18 | - import { remixDevTools } from 'remix-development-tools' 19 | + import { reactRouterDevTools } from 'react-router-devtools' 20 | 21 | export default defineConfig({ 22 | - plugins: [remixDevTools(), remix()], 23 | + plugins: [reactRouterDevTools(), reactRouter()], 24 | }) 25 | ``` 26 | 27 | 28 | And that's it! You should be good to go. If you have any issues, please reach out to us. -------------------------------------------------------------------------------- /docs/posts/main/guides/plugins.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Plugins" 3 | description: "React Router Devtools plugins" 4 | --- 5 | 6 | ## Plugins in Vite 7 | Plugins work in a different way in Vite. You create a directory for plugins and just provide the path to the directory to the plugin. The plugin will automatically import all the plugins from the directory and add them to the dev tools. You only need to make sure your exports are named exports and not default exports and that they are uniquely named. 8 | 9 | You can create a directory called plugins in your project and add your plugins there. Then you can add the following to your vite.config.js file: 10 | ```ts 11 | import { reactRouterDevTools } from "react-router-devtools"; 12 | export default defineConfig({ 13 | plugins: [ 14 | reactRouterDevTools({ 15 | pluginDir: "./plugins" 16 | })], 17 | }); 18 | ``` 19 | 20 | After you're done share your plugin with the community by opening a discussion and a PR with the plugin to the repo! -------------------------------------------------------------------------------- /docs/posts/main/started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | order: 1 4 | --- -------------------------------------------------------------------------------- /docs/posts/versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.0.0", 4 | "tag": "main" 5 | }, 6 | { 7 | "version": "1.0.0", 8 | "tag": "1.0.0" 9 | } 10 | ] -------------------------------------------------------------------------------- /docs/public/AI-Hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/AI-Hero.webp -------------------------------------------------------------------------------- /docs/public/active-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/active-page.png -------------------------------------------------------------------------------- /docs/public/active-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/active-tab.png -------------------------------------------------------------------------------- /docs/public/boundaries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/boundaries.png -------------------------------------------------------------------------------- /docs/public/error-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/error-tab.png -------------------------------------------------------------------------------- /docs/public/errors-tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/errors-tab.gif -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/fonts/FiraCode/FiraCode-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/fonts/FiraCode/FiraCode-Medium.ttf -------------------------------------------------------------------------------- /docs/public/fonts/FiraCode/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/fonts/FiraCode/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /docs/public/fonts/Inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/fonts/Inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /docs/public/fonts/Inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/fonts/Inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /docs/public/fonts/Space/Space.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/fonts/Space/Space.woff2 -------------------------------------------------------------------------------- /docs/public/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/logs.png -------------------------------------------------------------------------------- /docs/public/mask.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /docs/public/rdt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/rdt.png -------------------------------------------------------------------------------- /docs/public/routes-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/routes-tab.png -------------------------------------------------------------------------------- /docs/public/routes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/routes.gif -------------------------------------------------------------------------------- /docs/public/timeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/timeline.gif -------------------------------------------------------------------------------- /docs/public/tree-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/docs/public/tree-view.png -------------------------------------------------------------------------------- /docs/tests/e2e/link-checker.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test('doc navigation test', async ({ page }) => { 4 | await page.goto('/docs/main') 5 | await page.click("text=Beginner's Guide") 6 | await expect(page).toHaveURL('/docs/main/quick-start') 7 | }) 8 | -------------------------------------------------------------------------------- /docs/tests/setup/global-setup.ts: -------------------------------------------------------------------------------- 1 | export async function setup() { 2 | // add global setup logic here 3 | return async function teardown() {} 4 | } 5 | -------------------------------------------------------------------------------- /docs/tests/setup/setup-env-test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { installGlobals } from '@react-router/node' 3 | import { afterAll, afterEach } from 'vitest' 4 | 5 | installGlobals() 6 | 7 | // one time setup here 8 | 9 | afterEach(() => {}) 10 | afterAll(async () => {}) 11 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022", "WebWorker"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "allowImportingTsExtensions": true, 8 | "jsx": "react-jsx", 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "resolveJsonModule": true, 12 | "target": "ES2022", 13 | "strict": true, 14 | "allowJs": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["./app/*"] 20 | }, 21 | 22 | // Remix takes care of building everything in `remix build`. 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { reactRouter } from '@react-router/dev/vite' 3 | import { defineConfig } from 'vite' 4 | import { reactRouterDevTools } from 'react-router-devtools' 5 | import tsconfigPaths from 'vite-tsconfig-paths' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | reactRouterDevTools({ 10 | includeInProd: { 11 | client: true, 12 | server: true 13 | }, 14 | client: { position: 'middle-right' }, 15 | server: { silent: true }, 16 | }), 17 | reactRouter(), 18 | tsconfigPaths(), 19 | ], 20 | server: { 21 | open: true, 22 | port: 3000, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /docs/vitest.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import react from '@vitejs/plugin-react' 4 | import { defineConfig } from 'vite' 5 | import tsconfigPaths from 'vite-tsconfig-paths' 6 | 7 | export default defineConfig({ 8 | plugins: [react(), tsconfigPaths()], 9 | css: { postcss: { plugins: [] } }, 10 | test: { 11 | include: ['./app/**/*.test.{ts,tsx}'], 12 | environment: 'jsdom', 13 | setupFiles: ['./tests/setup/setup-env-test.ts'], 14 | globalSetup: ['./tests/setup/global-setup.ts'], 15 | coverage: { 16 | include: ['app/**/*.{ts,tsx}'], 17 | all: true, 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@5/schema.json", 3 | "workspaces": { 4 | ".": { 5 | "entry": ["scripts/*.ts"] 6 | } 7 | }, 8 | "ignore": ["docs/**", "plugins/**"], 9 | "ignoreBinaries": ["pkg-pr-new"], 10 | "ignoreWorkspaces": ["test-apps/**"] 11 | } 12 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | check: 5 | run: npm run check -- --staged --fix --no-errors-on-unmatched 6 | stage_fixed: true 7 | typecheck: 8 | run: npm run typecheck 9 | test: 10 | run: npm run test 11 | unused-code: 12 | run: npm run check:unused 13 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | This folder contains all the ready to go plugins that you can copy/paste into your project. 4 | 5 | If you build a plugin please feel free to create a PR towards the `plugins` folder so we can add it to the list of plugins. 6 | 7 | ## How to use 8 | 9 | Each plugin has a `README.md` file that explains how to use it. 10 | 11 | 12 | So all you need to do is find the one you like, copy the code and paste it into your project and follow the instructions from the `README.md` file. 13 | 14 | ## Can I add my own features? 15 | 16 | All the plugins featured under this folder are meant to be copy/pasted into your project with you having all the rights to modify them as you see fit. Feel free to add/remove whatever you like. If you add something cool, please share it with us so we can add it to the list of plugins or improve the existing ones. 17 | 18 | ## How to add a new plugin 19 | 20 | 1. Fork the repo 21 | 2. Create a new folder under `plugins` with the name of your plugin 22 | 3. Add a `README.md` file with the instructions on how to use the plugin 23 | 4. Add TODO's in the code where the user needs to modify the code to their project specifications 24 | 5. Add an image/video/gif of the plugin in action 25 | 6. Create a PR towards the repository! -------------------------------------------------------------------------------- /plugins/icon-library/README.md: -------------------------------------------------------------------------------- 1 | # Icon library plugin 2 | 3 | This plugin allows you to see all your project icons in a new tab in react router devtools, copy the code and change their classes. 4 | 5 | 6 | 7 | ## How to use 8 | 9 | ### Vite 10 | 1. Create a plugin directory in your project. (eg on the root you can create a `your-path-here` folder) 11 | 2. Copy the code from the plugin located in this folder. and paste it into there (eg `your-path-here/icon-library.tsx`) 12 | 3. Specify the plugin directory in your vite config via the `pluginDir` option: 13 | 14 | ```js 15 | // vite.config.js 16 | export default { 17 | plugins: [reactRouterDevTools({ pluginDir: './your-path-here' })] 18 | } 19 | ``` 20 | 21 | ## How it works 22 | 23 | The plugin will use all the icons in your project that are provided to it and will display them in a grid with different sizes. 24 | 25 | You can click on the icon to copy the code of the icon to your clipboard. 26 | 27 | You can use the input on the top right to add custom classes to the component (eg. change colors on the fly). 28 | 29 | ## How to add icons 30 | 31 | This will be easy/hard depending on your project setup. The basic idea is to have an icon component that is reusable across the project by 32 | providing a different name and size to the component. If you do not have this you would probably need to do it in a different way. What is 33 | important is to have an array of icon names in the project that you can use to generate the icons. 34 | 35 | ## Can I add my own features? 36 | 37 | All the plugins featured under this folder are meant to be copy/pasted into your project with you having all the rights to modify them as you see fit. Feel free to add/remove whatever you like. If you add something cool, please share it with us so we can add it to the list of plugins or improve the existing ones. -------------------------------------------------------------------------------- /plugins/icon-library/icon-library.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/plugins/icon-library/icon-library.mp4 -------------------------------------------------------------------------------- /plugins/tailwind-palette/README.md: -------------------------------------------------------------------------------- 1 | # Tailwind palette plugin 2 | 3 | This plugin allows you to see all tailwind colors in a new tab in react router devtools, copy them and paste into code. 4 | 5 | 6 | 7 | ## How to use 8 | 9 | 10 | ### Vite 11 | 1. Create a plugin directory in your project. (eg on the root you can create a `your-path-here` folder) 12 | 2. Add the plugin directory path to the tailwind config file. 13 | ```ts 14 | //tailwind.config.ts 15 | export default { 16 | content: ["./your-path-here/**/*.{ts,tsx}"] 17 | } 18 | ``` 19 | 4. Copy the code from the plugin located in this folder. and paste it into there (eg `your-path-here/tailwind-palette.tsx`) 20 | 5. Specify the plugin directory in your vite config via the `pluginDir` option: 21 | 22 | ```js 23 | // vite.config.js 24 | export default { 25 | plugins: [reactRouterDevTools({ pluginDir: './your-path-here' })] 26 | } 27 | ``` 28 | 29 | 30 | ## How it works 31 | 32 | The plugin will use all the tailwind colors and list them for you so you can easily copy paste them and apply them to your elements 33 | 34 | You can click on the color to copy the name of the color to your clipboard. 35 | 36 | 37 | ## Can I add my own features? 38 | 39 | All the plugins featured under this folder are meant to be copy/pasted into your project with you having all the rights to modify them as you see fit. Feel free to add/remove whatever you like. If you add something cool, please share it with us so we can add it to the list of plugins or improve the existing ones. 40 | -------------------------------------------------------------------------------- /plugins/tailwind-palette/color-palette.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/plugins/tailwind-palette/color-palette.mp4 -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | import autoprefixer from "autoprefixer" 2 | import tailwindcss from "tailwindcss" 3 | import tailwindcssNesting from "tailwindcss/nesting/index.js" 4 | import config from "./tailwind.config.js" 5 | 6 | /** @type {import('postcss').Config} */ 7 | export default { 8 | plugins: [tailwindcssNesting(), tailwindcss(config), autoprefixer()], 9 | } 10 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | If this is a new feature please add a description of what was added and why below: 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | # How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 21 | 22 | - [ ] Unit tests 23 | 24 | # Checklist: 25 | 26 | - [ ] My code follows the guidelines of this project 27 | - [ ] I have performed a self-review of my own code 28 | - [ ] I have commented my code, particularly in hard-to-understand areas 29 | - [ ] I have made corresponding changes to the documentation 30 | - [ ] My changes generate no new warnings or errors 31 | - [ ] I have added tests that prove my fix is effective or that my feature works 32 | - [ ] New and existing unit tests pass locally with my changes 33 | - [ ] Any dependent changes have been merged and published in downstream modules 34 | -------------------------------------------------------------------------------- /resources/icons/accessibility.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/icons/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/icons/columns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/copy-slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/icons/corner-down-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/git-merge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/icons/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /resources/icons/layout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icons/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/icons/network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/icons/radio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/icons/root.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/shield.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icons/terminal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | // Named exports 2 | export { EmbeddedDevTools } from "./client/embedded-dev-tools.js" 3 | export { withViteDevTools } from "./client/init/root.js" 4 | export { defineClientConfig } from "./client/init/root.js" 5 | export { withClientLoaderWrapper, withClientActionWrapper, withLinksWrapper } from "./client/hof.js" 6 | -------------------------------------------------------------------------------- /src/client/components/Breakpoints.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import { useSettingsContext } from "../context/useRDTContext" 3 | import { useOnWindowResize } from "../hooks/useOnWindowResize" 4 | 5 | export const Breakpoints = () => { 6 | const { width } = useOnWindowResize() 7 | const { settings } = useSettingsContext() 8 | const breakpoints = settings.breakpoints 9 | const show = settings.showBreakpointIndicator 10 | const breakpoint = breakpoints.find((bp) => bp.min <= width && bp.max >= width) 11 | if (!breakpoint || !breakpoint.name || !show) { 12 | return null 13 | } 14 | return ( 15 |
20 | {breakpoint?.name} 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/client/components/CacheInfo.tsx: -------------------------------------------------------------------------------- 1 | import { add } from "date-fns/add" 2 | import { formatDistance } from "date-fns/formatDistance" 3 | import type { CacheControl } from "../../server/parser.js" 4 | import { useCountdown } from "../hooks/useCountdown.js" 5 | import { Tag } from "./Tag.js" 6 | 7 | interface CacheInfoProps { 8 | cacheDate: Date 9 | cacheControl: CacheControl 10 | } 11 | 12 | const CacheInfo = ({ cacheDate, cacheControl }: CacheInfoProps) => { 13 | const { maxAge, sMaxage, private: isPrivate } = cacheControl 14 | 15 | const age = !isPrivate && !maxAge ? sMaxage : maxAge 16 | const targetDate = add(cacheDate, { seconds: age ? Number.parseInt(age) : 0 }) 17 | const { minutes, seconds, stringRepresentation } = useCountdown(targetDate) 18 | const distance = formatDistance(targetDate, cacheDate, { addSuffix: true }) 19 | if (seconds <= 0) { 20 | return 21 | } 22 | return ( 23 | 24 | [{cacheControl.private ? "Private" : "Shared"}] Loader Cache expires {distance} ({stringRepresentation}) 25 | 26 | ) 27 | } 28 | 29 | export { CacheInfo } 30 | -------------------------------------------------------------------------------- /src/client/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | interface CheckboxProps extends Omit, "value"> { 2 | onChange?: (e: React.ChangeEvent) => void 3 | id: string 4 | children: React.ReactNode 5 | value?: boolean 6 | hint?: string 7 | } 8 | 9 | const Checkbox = ({ onChange, id, children, value, hint, ...props }: CheckboxProps) => { 10 | return ( 11 |
12 | 26 | {hint &&

{hint}

} 27 |
28 | ) 29 | } 30 | 31 | export { Checkbox } 32 | -------------------------------------------------------------------------------- /src/client/components/EditorButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | 3 | interface EditorButtonProps extends React.HTMLAttributes { 4 | onClick: () => void 5 | name: string 6 | } 7 | 8 | const EditorButton = ({ name, onClick, ...props }: EditorButtonProps) => { 9 | return ( 10 | 20 | ) 21 | } 22 | 23 | export { EditorButton } 24 | -------------------------------------------------------------------------------- /src/client/components/InfoCard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import type { ReactNode } from "react" 3 | 4 | export const InfoCard = ({ 5 | children, 6 | title, 7 | onClear, 8 | }: { 9 | children: ReactNode 10 | title: string 11 | onClear?: () => void 12 | }) => { 13 | return ( 14 |
15 |

21 | {title} 22 | {onClear && typeof import.meta.hot === "undefined" && ( 23 | 30 | )} 31 |

32 | 33 | {children} 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/client/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | 3 | interface InputProps extends React.InputHTMLAttributes { 4 | label?: string 5 | hint?: string 6 | } 7 | 8 | export const Label = ({ className, children, ...props }: React.HTMLProps) => { 9 | return ( 10 | 13 | ) 14 | } 15 | 16 | export const Hint = ({ children }: React.HTMLProps) => { 17 | return

{children}

18 | } 19 | 20 | const Input = ({ className, name, label, hint, ...props }: InputProps) => { 21 | return ( 22 |
23 | {label && } 24 | 33 | {hint && {hint}} 34 |
35 | ) 36 | } 37 | 38 | export { Input } 39 | -------------------------------------------------------------------------------- /src/client/components/LiveUrls.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import { Link, useLocation } from "react-router" 3 | import { useSettingsContext } from "../context/useRDTContext" 4 | 5 | export const LiveUrls = () => { 6 | const { settings } = useSettingsContext() 7 | const location = useLocation() 8 | const envsPosition = settings.liveUrlsPosition 9 | const envsClassName = { 10 | "bottom-0": envsPosition === "bottom-left" || envsPosition === "bottom-right", 11 | "top-0": envsPosition === "top-left" || envsPosition === "top-right", 12 | "right-0": envsPosition === "bottom-right" || envsPosition === "top-right", 13 | "left-0": envsPosition === "bottom-left" || envsPosition === "top-left", 14 | } 15 | if (settings.liveUrls.length === 0) return null 16 | return ( 17 |
18 | {settings.liveUrls.map((env) => { 19 | return ( 20 | 27 | {env.name} 28 | 29 | ) 30 | })} 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/client/components/RouteToggle.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import { useSettingsContext } from "../context/useRDTContext.js" 3 | import { Icon } from "./icon/Icon.js" 4 | 5 | export const RouteToggle = () => { 6 | const { settings, setSettings } = useSettingsContext() 7 | const { routeViewMode } = settings 8 | return ( 9 |
10 | setSettings({ routeViewMode: "tree" })} 13 | name="Network" 14 | /> 15 | / 16 | setSettings({ routeViewMode: "list" })} 20 | /> 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/client/components/Stack.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | 3 | interface StackProps extends React.HTMLAttributes { 4 | gap?: "sm" | "md" | "lg" 5 | } 6 | 7 | const GAPS = { 8 | sm: "gap-1", 9 | md: "gap-2", 10 | lg: "gap-4", 11 | } 12 | 13 | const Stack = ({ gap = "md", className, children, ...props }: StackProps) => { 14 | return ( 15 |
16 | {children} 17 |
18 | ) 19 | } 20 | 21 | export { Stack } 22 | -------------------------------------------------------------------------------- /src/client/components/Tag.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import type { ReactNode } from "react" 3 | 4 | export const TAG_COLORS = { 5 | GREEN: "border-green-500 border border-solid text-white", 6 | BLUE: "border-blue-500 border border-solid text-white", 7 | TEAL: "border-teal-400 border border-solid text-white", 8 | RED: "border-red-500 border border-solid text-white", 9 | PURPLE: "border-purple-500 border border-solid text-white", 10 | } as const 11 | 12 | interface TagProps { 13 | color: keyof typeof TAG_COLORS 14 | children: ReactNode 15 | className?: string 16 | } 17 | 18 | const Tag = ({ color, children, className }: TagProps) => { 19 | return ( 20 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | export { Tag } 27 | -------------------------------------------------------------------------------- /src/client/components/icon/icons/types.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by npm run build:icons 2 | 3 | export type IconName = (typeof iconNames)[number] 4 | 5 | const iconNames = [ 6 | "X", 7 | "Terminal", 8 | "Settings", 9 | "Send", 10 | "Radio", 11 | "Network", 12 | "List", 13 | "Layers", 14 | "GitMerge", 15 | "CornerDownRight", 16 | "CopySlash", 17 | "Columns", 18 | "ChevronDown", 19 | "Check", 20 | "Activity", 21 | "Accessibility", 22 | "Shield", 23 | "Root", 24 | "Layout", 25 | ] as const 26 | -------------------------------------------------------------------------------- /src/client/components/network-tracer/NetworkPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { useRequestContext } from "../../context/requests/request-context" 3 | 4 | import NetworkWaterfall from "./NetworkWaterfall" 5 | 6 | function NetworkPanel() { 7 | const { requests } = useRequestContext() 8 | 9 | const [containerWidth, setContainerWidth] = useState(800) 10 | 11 | // Simulate network requests for demo 12 | 13 | // Update container width on resize 14 | useEffect(() => { 15 | const updateWidth = () => { 16 | const container = document.querySelector(".network-container") 17 | if (container) { 18 | setContainerWidth(container.clientWidth) 19 | } 20 | } 21 | 22 | window.addEventListener("resize", updateWidth) 23 | updateWidth() 24 | 25 | return () => window.removeEventListener("resize", updateWidth) 26 | }, []) 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export default NetworkPanel 42 | -------------------------------------------------------------------------------- /src/client/components/util.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/client/context/requests/request-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useCallback, useContext, useEffect, useState } from "react" 2 | import type { RequestEvent } from "../../../shared/request-event" 3 | 4 | const RequestContext = createContext<{ 5 | requests: RequestEvent[] 6 | removeAllRequests: () => void 7 | }>({ requests: [], removeAllRequests: () => {} }) 8 | 9 | const requestMap = new Map() 10 | 11 | export const RequestProvider = ({ children }: any) => { 12 | const [requests, setRequests] = useState([]) 13 | const setNewRequests = useCallback((payload: string) => { 14 | const requests = JSON.parse(payload) 15 | const newRequests = Array.isArray(requests) ? requests : [requests] 16 | for (const req of newRequests) { 17 | requestMap.set(req.id + req.startTime, req) 18 | import.meta.hot?.send("remove-event", { ...req, fromClient: true }) 19 | } 20 | setRequests(Array.from(requestMap.values())) 21 | }, []) 22 | useEffect(() => { 23 | import.meta.hot?.send("get-events") 24 | import.meta.hot?.on("get-events", setNewRequests) 25 | import.meta.hot?.on("request-event", setNewRequests) 26 | return () => { 27 | import.meta.hot?.off?.("get-events", setNewRequests) 28 | import.meta.hot?.off?.("request-event", setNewRequests) 29 | } 30 | }, [setNewRequests]) 31 | 32 | const removeAllRequests = useCallback(() => { 33 | setRequests([]) 34 | requestMap.clear() 35 | }, []) 36 | return {children} 37 | } 38 | 39 | export const useRequestContext = () => { 40 | return useContext(RequestContext) 41 | } 42 | -------------------------------------------------------------------------------- /src/client/context/terminal/types.ts: -------------------------------------------------------------------------------- 1 | interface TerminalOutput { 2 | type: "output" | "command" | "error" 3 | value: string 4 | } 5 | 6 | export interface Terminal { 7 | id: number 8 | locked: boolean 9 | history: string[] 10 | output: TerminalOutput[] 11 | processId?: number 12 | } 13 | -------------------------------------------------------------------------------- /src/client/context/timeline/types.ts: -------------------------------------------------------------------------------- 1 | import type { FormEncType } from "react-router" 2 | 3 | interface NormalRedirectEvent { 4 | type: "REDIRECT" 5 | to: string 6 | search: string 7 | hash: string 8 | method: "GET" 9 | id: string 10 | responseData?: Record 11 | } 12 | interface FetcherRedirectEvent extends Omit { 13 | type: "FETCHER_REDIRECT" 14 | } 15 | 16 | interface FetcherSubmissionEvent extends Omit { 17 | type: "FETCHER_SUBMIT" 18 | key?: string 19 | responseData?: Record 20 | } 21 | interface FetcherSubmissionResponseEvent extends Omit { 22 | type: "FETCHER_RESPONSE" 23 | key?: string 24 | responseData?: Record 25 | } 26 | interface FormSubmissionEvent { 27 | type: "FORM_SUBMISSION" 28 | id: string 29 | to: string 30 | data?: Record 31 | responseData?: Record 32 | method: "get" | "post" | "put" | "patch" | "delete" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" 33 | from: string 34 | encType?: FormEncType 35 | } 36 | interface ActionRedirectEvent extends Omit { 37 | type: "ACTION_REDIRECT" 38 | } 39 | interface ActionResponseEvent extends Omit { 40 | type: "ACTION_RESPONSE" 41 | } 42 | export type RedirectEvent = NormalRedirectEvent | FetcherRedirectEvent 43 | export type FormEvent = 44 | | FormSubmissionEvent 45 | | FetcherSubmissionEvent 46 | | ActionRedirectEvent 47 | | FetcherSubmissionResponseEvent 48 | | ActionResponseEvent 49 | 50 | export type TimelineEvent = RedirectEvent | FormEvent 51 | -------------------------------------------------------------------------------- /src/client/hooks/detached/useRemoveBody.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | import type { ReactRouterDevtoolsState } from "../../context/rdtReducer.js" 3 | import { REACT_ROUTER_DEV_TOOLS } from "../../utils/storage.js" 4 | 5 | export const useRemoveBody = (state: ReactRouterDevtoolsState) => { 6 | useEffect(() => { 7 | if (!state.detachedWindow) { 8 | return 9 | } 10 | 11 | const elements = document.body.children 12 | document.body.classList.add("bg-[#212121]") 13 | for (let i = 0; i < elements.length; i++) { 14 | const element = elements[i] 15 | if (element.id !== REACT_ROUTER_DEV_TOOLS) { 16 | element.classList.add("hidden") 17 | } 18 | } 19 | }, [state]) 20 | } 21 | -------------------------------------------------------------------------------- /src/client/hooks/detached/useResetDetachmentCheck.ts: -------------------------------------------------------------------------------- 1 | import { useDetachedWindowControls } from "../../context/useRDTContext.js" 2 | import { REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, setStorageItem } from "../../utils/storage.js" 3 | import { useAttachListener } from "../useAttachListener.js" 4 | import { useCheckIfStillDetached } from "./useCheckIfStillDetached.js" 5 | 6 | export const useResetDetachmentCheck = () => { 7 | const { isDetached } = useDetachedWindowControls() 8 | useCheckIfStillDetached() 9 | useAttachListener("unload", "window", () => setStorageItem(REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED, "true"), isDetached) 10 | } 11 | -------------------------------------------------------------------------------- /src/client/hooks/detached/useSyncStateWhenDetached.ts: -------------------------------------------------------------------------------- 1 | import { getExistingStateFromStorage } from "../../context/RDTContext.js" 2 | import { useRDTContext } from "../../context/useRDTContext.js" 3 | import { REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE } from "../../utils/storage.js" 4 | import { useAttachListener } from "../useAttachListener.js" 5 | 6 | const refreshRequiredKeys = [REACT_ROUTER_DEV_TOOLS_SETTINGS, REACT_ROUTER_DEV_TOOLS_STATE] 7 | 8 | export const useSyncStateWhenDetached = () => { 9 | const { dispatch, state } = useRDTContext() 10 | 11 | useAttachListener("storage", "window", (e: any) => { 12 | // Not in detached mode 13 | if (!state.detachedWindow && !state.detachedWindowOwner) { 14 | return 15 | } 16 | // Not caused by the dev tools 17 | if (!refreshRequiredKeys.includes(e.key)) { 18 | return 19 | } 20 | // Check if the settings have not changed and early return 21 | if (e.key === REACT_ROUTER_DEV_TOOLS_SETTINGS) { 22 | const oldSettings = JSON.stringify(state.settings) 23 | if (oldSettings === e.newValue) { 24 | return 25 | } 26 | } 27 | // Check if the state has not changed and early return 28 | if (e.key === REACT_ROUTER_DEV_TOOLS_STATE) { 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | const { settings, ...rest } = state 31 | const oldState = JSON.stringify(rest) 32 | if (oldState === e.newValue) { 33 | return 34 | } 35 | } 36 | 37 | // store new state 38 | const newState = getExistingStateFromStorage() 39 | dispatch({ type: "SET_WHOLE_STATE", payload: newState }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/client/hooks/useCountdown.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | const getTimeLeft = (countDown: number) => { 4 | // calculate time left 5 | const days = Math.floor(countDown / (1000 * 60 * 60 * 24)) 6 | const hours = Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) 7 | const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)) 8 | const seconds = Math.floor((countDown % (1000 * 60)) / 1000) 9 | 10 | return { days, hours, minutes, seconds } 11 | } 12 | 13 | const useCountdown = (targetDate: string | Date) => { 14 | const countDownDate = new Date(targetDate).getTime() 15 | 16 | const [countDown, setCountDown] = useState(countDownDate - new Date().getTime()) 17 | 18 | // biome-ignore lint/correctness/useExhaustiveDependencies: investigate 19 | useEffect(() => { 20 | const timeLeft = getTimeLeft(countDown) 21 | if (timeLeft.seconds <= 0) { 22 | return 23 | } 24 | const interval = setInterval(() => { 25 | setCountDown(countDownDate - new Date().getTime()) 26 | }, 1000) 27 | 28 | return () => clearInterval(interval) 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, [countDownDate]) 31 | 32 | const timeLeft = getTimeLeft(countDown) 33 | const stringRepresentation = `${timeLeft.days > 0 ? `${timeLeft.days}d ` : ""}${ 34 | timeLeft.hours ? `${timeLeft.hours}h ` : "" 35 | }${timeLeft.minutes ? `${timeLeft.minutes}m ` : ""}${timeLeft.seconds ? `${timeLeft.seconds}s` : ""}` 36 | return { ...timeLeft, stringRepresentation } 37 | } 38 | 39 | export { useCountdown } 40 | -------------------------------------------------------------------------------- /src/client/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | function debounce(func: (...args: any[]) => any, timeout = 300) { 4 | let timer: NodeJS.Timeout 5 | return (...args: any[]) => { 6 | clearTimeout(timer) 7 | timer = setTimeout(() => { 8 | /* @ts-ignore */ 9 | func.apply(this, args) 10 | }, timeout) 11 | } 12 | } 13 | 14 | export function useDebounce(callback: (...args: any[]) => void, delay = 300) { 15 | const callbackRef = React.useRef(callback) 16 | React.useEffect(() => { 17 | callbackRef.current = callback 18 | }) 19 | return React.useMemo(() => debounce((...args) => callbackRef.current(...args), delay), [delay]) 20 | } 21 | -------------------------------------------------------------------------------- /src/client/hooks/useHorizontalScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react" 2 | 3 | export const useHorizontalScroll = () => { 4 | const ref = useRef(null) 5 | 6 | useEffect(() => { 7 | const elem = ref.current 8 | const onWheel = (ev: WheelEvent) => { 9 | if (!elem || ev.deltaY === 0) return 10 | 11 | elem.scrollTo({ 12 | left: elem.scrollLeft + ev.deltaY, 13 | behavior: "smooth", 14 | }) 15 | } 16 | 17 | elem?.addEventListener("wheel", onWheel, { passive: true }) 18 | 19 | return () => { 20 | elem?.removeEventListener("wheel", onWheel) 21 | } 22 | }, []) 23 | 24 | return ref 25 | } 26 | -------------------------------------------------------------------------------- /src/client/hooks/useOnWindowResize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | type WindowSize = { 4 | width: number 5 | height: number 6 | } 7 | export const useOnWindowResize = () => { 8 | const [windowSize, setWindowSize] = useState({ 9 | width: window.innerWidth, 10 | height: window.innerHeight, 11 | }) 12 | 13 | useEffect(() => { 14 | const handleResize = () => { 15 | setWindowSize({ 16 | width: window.innerWidth, 17 | height: window.innerHeight, 18 | }) 19 | } 20 | 21 | window.addEventListener("resize", handleResize) 22 | 23 | return () => { 24 | window.removeEventListener("resize", handleResize) 25 | } 26 | }, []) 27 | return windowSize 28 | } 29 | -------------------------------------------------------------------------------- /src/client/hooks/useOpenElementSource.ts: -------------------------------------------------------------------------------- 1 | import { useAttachDocumentListener } from "./useAttachListener.js" 2 | import { useDevServerConnection } from "./useDevServerConnection.js" 3 | 4 | const useOpenElementSource = () => { 5 | const { sendJsonMessage } = useDevServerConnection() 6 | 7 | useAttachDocumentListener("contextmenu", (e: any) => { 8 | if (!e.shiftKey || !e) { 9 | return 10 | } 11 | 12 | e.stopPropagation() 13 | e.preventDefault() 14 | const target = e.target as HTMLElement 15 | const rdtSource = target?.getAttribute("data-source") 16 | 17 | if (rdtSource) { 18 | const [source, line, column] = rdtSource.split(":::") 19 | return sendJsonMessage({ 20 | type: "open-source", 21 | data: { source, line, column }, 22 | }) 23 | } 24 | for (const key in e.target) { 25 | if (key.startsWith("__reactFiber")) { 26 | const fiberNode = (e.target as any)[key] 27 | 28 | const originalSource = fiberNode?._debugSource 29 | const source = fiberNode?._debugOwner?._debugSource ?? fiberNode?._debugSource 30 | const line = source?.fileName?.startsWith("/") ? originalSource?.lineNumber : source?.lineNumber 31 | const fileName = source?.fileName?.startsWith("/") ? originalSource?.fileName : source?.fileName 32 | if (fileName && line) { 33 | return sendJsonMessage({ 34 | type: "open-source", 35 | data: { source: fileName, line, column: 0 }, 36 | }) 37 | } 38 | } 39 | } 40 | }) 41 | } 42 | 43 | export { useOpenElementSource } 44 | -------------------------------------------------------------------------------- /src/client/hooks/useTabs.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from "react" 2 | import type { ReactRouterDevtoolsState } from "../context/rdtReducer.js" 3 | import { useSettingsContext } from "../context/useRDTContext.js" 4 | import type { ReactRouterDevtoolsProps } from "../react-router-dev-tools.js" 5 | import { type Tab, tabs } from "../tabs/index.js" 6 | import type { Tabs } from "../tabs/index.js" 7 | 8 | const shouldHideTimeline = (activeTab: Tabs, tab: Tab | undefined, settings: ReactRouterDevtoolsState["settings"]) => { 9 | if (activeTab === "routes" && settings.routeViewMode === "tree") return true 10 | return tab?.hideTimeline 11 | } 12 | 13 | export const useTabs = (pluginsArray?: ReactRouterDevtoolsProps["plugins"]) => { 14 | const { settings } = useSettingsContext() 15 | const { activeTab } = settings 16 | const plugins = pluginsArray?.map((plugin) => (typeof plugin === "function" ? plugin() : plugin)) 17 | const allTabs = useMemo(() => [...tabs, ...(plugins ? plugins : [])], [plugins]) 18 | 19 | const { Component, hideTimeline } = useMemo(() => { 20 | const tab = allTabs.find((tab) => tab.id === activeTab) 21 | return { Component: tab?.component, hideTimeline: shouldHideTimeline(activeTab, tab, settings) } 22 | }, [activeTab, allTabs, settings]) 23 | 24 | return { 25 | visibleTabs: allTabs, 26 | Component, 27 | allTabs, 28 | hideTimeline, 29 | activeTab, 30 | isPluginTab: !tabs.find((tab) => activeTab === tab.id), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/init/root.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { createPortal } from "react-dom" 3 | import type { RdtClientConfig } from "../context/RDTContext.js" 4 | import { RequestProvider } from "../context/requests/request-context.js" 5 | import { ReactRouterDevTools, type ReactRouterDevtoolsProps } from "../react-router-dev-tools.js" 6 | import { hydrationDetector } from "./hydration.js" 7 | 8 | let hydrating = true 9 | 10 | function useHydrated() { 11 | const [hydrated, setHydrated] = useState(() => !hydrating) 12 | 13 | useEffect(function hydrate() { 14 | hydrating = false 15 | setHydrated(true) 16 | }, []) 17 | 18 | return hydrated 19 | } 20 | 21 | export const defineClientConfig = (config: RdtClientConfig) => config 22 | 23 | /** 24 | * 25 | * @description Injects the dev tools into the Vite App, ONLY meant to be used by the package plugin, do not use this yourself! 26 | */ 27 | export const withViteDevTools = (Component: any, config?: ReactRouterDevtoolsProps) => (props: any) => { 28 | hydrationDetector() 29 | function AppWithDevTools(props: any) { 30 | const hydrated = useHydrated() 31 | if (!hydrated) 32 | return ( 33 | 34 | 35 | 36 | ) 37 | return ( 38 | 39 | 40 | {createPortal(, document.body)} 41 | 42 | ) 43 | } 44 | return AppWithDevTools(props) 45 | } 46 | -------------------------------------------------------------------------------- /src/client/layout/ContentPanel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import { Fragment } from "react" 3 | import { useTabs } from "../hooks/useTabs.js" 4 | import { TimelineTab } from "../tabs/TimelineTab.js" 5 | import type { Tab } from "../tabs/index.js" 6 | 7 | interface ContentPanelProps { 8 | leftSideOriented: boolean 9 | plugins?: Tab[] 10 | } 11 | 12 | const ContentPanel = ({ plugins }: ContentPanelProps) => { 13 | const { Component, hideTimeline, isPluginTab, activeTab } = useTabs(plugins) 14 | 15 | return ( 16 |
17 |
25 | {Component} 26 |
27 | 28 | {!hideTimeline && ( 29 | 30 |
31 | 34 | 35 | )} 36 |
37 | ) 38 | } 39 | 40 | export { ContentPanel } 41 | -------------------------------------------------------------------------------- /src/client/tabs/NetworkTab.tsx: -------------------------------------------------------------------------------- 1 | import NetworkPanel from "../components/network-tracer/NetworkPanel" 2 | 3 | export const NetworkTab = () => { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/client/tabs/PageTab.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import { useMemo } from "react" 3 | import { useMatches, useRevalidator } from "react-router" 4 | 5 | import { RouteSegmentInfo } from "../components/RouteSegmentInfo.js" 6 | 7 | const PageTab = () => { 8 | const routes = useMatches() 9 | const reversed = useMemo(() => routes.reverse(), [routes]) 10 | const { revalidate, state } = useRevalidator() 11 | 12 | return ( 13 | <> 14 |
15 |
16 |
Active Route Segments
17 | 28 |
29 |
30 |
31 |
32 |
    35 | {reversed.map((route, i) => ( 36 | 37 | ))} 38 |
39 |
40 | 41 | ) 42 | } 43 | 44 | export { PageTab } 45 | -------------------------------------------------------------------------------- /src/client/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from "react" 2 | import { Icon } from "../components/icon/Icon.js" 3 | import { ErrorsTab } from "./ErrorsTab.js" 4 | import { NetworkTab } from "./NetworkTab.js" 5 | import { PageTab } from "./PageTab.js" 6 | import { RoutesTab } from "./RoutesTab.js" 7 | import { SettingsTab } from "./SettingsTab.js" 8 | 9 | export type Tabs = (typeof tabs)[number]["id"] 10 | 11 | export interface Tab { 12 | name: string | JSX.Element 13 | icon: JSX.Element 14 | id: string 15 | component: JSX.Element 16 | hideTimeline: boolean 17 | } 18 | 19 | export const tabs = [ 20 | { 21 | name: "Active page", 22 | icon: , 23 | id: "page", 24 | component: , 25 | hideTimeline: false, 26 | }, 27 | { 28 | name: "Routes", 29 | icon: , 30 | id: "routes", 31 | component: , 32 | hideTimeline: false, 33 | }, 34 | 35 | { 36 | name: "Errors", 37 | icon: , 38 | id: "errors", 39 | component: , 40 | 41 | hideTimeline: false, 42 | }, 43 | { 44 | name: "Network", 45 | icon: , 46 | id: "network", 47 | component: , 48 | 49 | hideTimeline: true, 50 | }, 51 | { 52 | name: "Settings", 53 | icon: , 54 | id: "settings", 55 | component: , 56 | hideTimeline: false, 57 | }, 58 | ] as const 59 | -------------------------------------------------------------------------------- /src/client/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const cutArrayToLastN = (arr: T[], n: number) => { 2 | if (arr.length < n) return arr 3 | return arr.slice(arr.length - n) 4 | } 5 | 6 | export const cutArrayToFirstN = (arr: T[], n: number) => { 7 | if (arr.length < n) return arr 8 | return arr.slice(0, n) 9 | } 10 | -------------------------------------------------------------------------------- /src/client/utils/detached.ts: -------------------------------------------------------------------------------- 1 | import { 2 | REACT_ROUTER_DEV_TOOLS_DETACHED, 3 | REACT_ROUTER_DEV_TOOLS_DETACHED_OWNER, 4 | REACT_ROUTER_DEV_TOOLS_IS_DETACHED, 5 | getBooleanFromSession, 6 | getBooleanFromStorage, 7 | } from "./storage.js" 8 | 9 | export const checkIsDetachedWindow = () => getBooleanFromSession(REACT_ROUTER_DEV_TOOLS_DETACHED) 10 | export const checkIsDetached = () => getBooleanFromStorage(REACT_ROUTER_DEV_TOOLS_IS_DETACHED) 11 | export const checkIsDetachedOwner = () => getBooleanFromSession(REACT_ROUTER_DEV_TOOLS_DETACHED_OWNER) 12 | -------------------------------------------------------------------------------- /src/client/utils/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getBooleanFromSession, 3 | getBooleanFromStorage, 4 | getSessionItem, 5 | getStorageItem, 6 | setSessionItem, 7 | setStorageItem, 8 | } from "./storage.js" 9 | 10 | describe("storage utils", () => { 11 | beforeEach(() => { 12 | localStorage.clear() 13 | sessionStorage.clear() 14 | }) 15 | 16 | test("getStorageItem should return null if key does not exist", () => { 17 | expect(getStorageItem("non-existent-key")).toBeNull() 18 | }) 19 | 20 | test("getSessionItem should return null if key does not exist", () => { 21 | expect(getSessionItem("non-existent-key")).toBeNull() 22 | }) 23 | 24 | test("setStorageItem should set the value for the given key in localStorage", () => { 25 | setStorageItem("key", "value") 26 | expect(localStorage.getItem("key")).toBe("value") 27 | }) 28 | 29 | test("setSessionItem should set the value for the given key in sessionStorage", () => { 30 | setSessionItem("key", "value") 31 | expect(sessionStorage.getItem("key")).toBe("value") 32 | }) 33 | 34 | test('getBooleanFromStorage should return true if the value for the given key is "true"', () => { 35 | localStorage.setItem("key", "true") 36 | expect(getBooleanFromStorage("key")).toBe(true) 37 | }) 38 | 39 | test('getBooleanFromStorage should return false if the value for the given key is not "true"', () => { 40 | localStorage.setItem("key", "false") 41 | expect(getBooleanFromStorage("key")).toBe(false) 42 | }) 43 | 44 | test('getBooleanFromSession should return true if the value for the given key is "true"', () => { 45 | sessionStorage.setItem("key", "true") 46 | expect(getBooleanFromSession("key")).toBe(true) 47 | }) 48 | 49 | test('getBooleanFromSession should return false if the value for the given key is not "true"', () => { 50 | sessionStorage.setItem("key", "false") 51 | expect(getBooleanFromSession("key")).toBe(false) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/client/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export const getStorageItem = (key: string) => localStorage.getItem(key) 2 | export const setStorageItem = (key: string, value: string) => { 3 | try { 4 | localStorage.setItem(key, value) 5 | } catch (e) { 6 | return 7 | } 8 | } 9 | export const getSessionItem = (key: string) => sessionStorage.getItem(key) 10 | export const setSessionItem = (key: string, value: string) => { 11 | try { 12 | sessionStorage.setItem(key, value) 13 | } catch (e) { 14 | return 15 | } 16 | } 17 | 18 | export const getBooleanFromStorage = (key: string) => getStorageItem(key) === "true" 19 | export const getBooleanFromSession = (key: string) => getSessionItem(key) === "true" 20 | 21 | export const REACT_ROUTER_DEV_TOOLS = "react_router_devtools" 22 | export const REACT_ROUTER_DEV_TOOLS_STATE = "react_router_devtools_state" 23 | export const REACT_ROUTER_DEV_TOOLS_SETTINGS = "react_router_devtools_settings" 24 | export const REACT_ROUTER_DEV_TOOLS_DETACHED = "react_router_devtools_detached" 25 | export const REACT_ROUTER_DEV_TOOLS_DETACHED_OWNER = "react_router_devtools_detached_owner" 26 | export const REACT_ROUTER_DEV_TOOLS_IS_DETACHED = "react_router_devtools_is_detached" 27 | export const REACT_ROUTER_DEV_TOOLS_CHECK_DETACHED = "react_router_devtools_check_detached" 28 | -------------------------------------------------------------------------------- /src/client/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const uppercaseFirstLetter = (value: string) => value.charAt(0).toUpperCase() + value.slice(1) 2 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | withActionContextWrapper, 3 | withClientActionContextWrapper, 4 | withClientLoaderContextWrapper, 5 | withLoaderContextWrapper, 6 | } from "./context/extend-context" 7 | 8 | export { 9 | withLoaderContextWrapper, 10 | withActionContextWrapper, 11 | withClientLoaderContextWrapper, 12 | withClientActionContextWrapper, 13 | } 14 | export type { ExtendedContext } from "./context/extend-context" 15 | -------------------------------------------------------------------------------- /src/external/react-json-view/Container.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useId } from 'react'; 2 | import { NestedClose } from './comps/NestedClose.js'; 3 | import { NestedOpen } from './comps/NestedOpen.js'; 4 | import { KeyValues } from './comps/KeyValues.js'; 5 | import { useShowToolsDispatch } from './store/ShowTools.js'; 6 | 7 | interface ContainerProps extends React.HTMLAttributes { 8 | keyName?: string | number; 9 | keyid?: string; 10 | parentValue?: T; 11 | level?: number; 12 | value?: T; 13 | initialValue?: T; 14 | } 15 | export const Container = forwardRef((props: ContainerProps, ref: React.Ref) => { 16 | const { className = '', children, parentValue, keyid, level = 1, value, initialValue, keyName, ...elmProps } = props; 17 | const dispatch = useShowToolsDispatch(); 18 | const subkeyid = useId(); 19 | const defaultClassNames = [className, 'w-rjv-inner'].filter(Boolean).join(' '); 20 | const reset: React.HTMLAttributes = { 21 | onMouseEnter: () => dispatch({ [subkeyid]: true }), 22 | onMouseLeave: () => dispatch({ [subkeyid]: false }), 23 | }; 24 | return ( 25 |
26 | 27 | 28 | 29 |
30 | ); 31 | }); 32 | 33 | Container.displayName = 'JVR.Container'; 34 | -------------------------------------------------------------------------------- /src/external/react-json-view/README.MD: -------------------------------------------------------------------------------- 1 | Full credit for the code in this folder goes to: 2 | https://github.com/uiwjs/react-json-view -------------------------------------------------------------------------------- /src/external/react-json-view/arrow/TriangleArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TriangleArrowProps = React.SVGProps 4 | export function TriangleArrow(props: TriangleArrowProps) { 5 | const { style, ...reset } = props; 6 | const defaultStyle: React.CSSProperties = { 7 | cursor: 'pointer', 8 | height: '1em', 9 | width: '1em', 10 | userSelect: 'none', 11 | display: 'flex', 12 | ...style, 13 | }; 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | TriangleArrow.displayName = 'JVR.TriangleArrow'; 22 | -------------------------------------------------------------------------------- /src/external/react-json-view/comps/NestedClose.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from "../store.js"; 2 | import { useExpandsStore } from "../store/Expands.js"; 3 | import { BracketsClose } from "../symbol/index.js"; 4 | 5 | interface NestedCloseProps { 6 | value?: T; 7 | expandKey: string; 8 | level: number; 9 | } 10 | 11 | export const NestedClose = (props: NestedCloseProps) => { 12 | const { value, expandKey, level } = props; 13 | const expands = useExpandsStore(); 14 | const isArray = Array.isArray(value); 15 | const { collapsed } = useStore(); 16 | const isMySet = value instanceof Set; 17 | const isExpanded = 18 | expands[expandKey] ?? 19 | (typeof collapsed === "boolean" ? collapsed : typeof collapsed === "number" ? level > collapsed : false); 20 | const len = Object.keys(value!).length; 21 | if (isExpanded || len === 0) { 22 | return null; 23 | } 24 | const style: React.CSSProperties = { 25 | paddingLeft: 4, 26 | }; 27 | return ( 28 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | NestedClose.displayName = "JVR.NestedClose"; 35 | -------------------------------------------------------------------------------- /src/external/react-json-view/comps/Value.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | TypeString, 3 | TypeTrue, 4 | TypeNull, 5 | TypeFalse, 6 | TypeFloat, 7 | TypeBigint, 8 | TypeInt, 9 | TypeDate, 10 | TypeUndefined, 11 | TypeNan, 12 | TypeUrl, 13 | } from '../types/index.js'; 14 | 15 | const isFloat = (n: number) => (Number(n) === n && n % 1 !== 0) || isNaN(n); 16 | 17 | interface ValueProps { 18 | value?: unknown; 19 | keyName: string | number; 20 | expandKey: string; 21 | } 22 | 23 | export const Value = (props: ValueProps) => { 24 | const { value, keyName, expandKey } = props; 25 | const reset = { keyName, expandKey }; 26 | if (value instanceof URL) { 27 | return {value}; 28 | } 29 | if (typeof value === 'string') { 30 | return {value}; 31 | } 32 | if (value === true) { 33 | return {value}; 34 | } 35 | if (value === false) { 36 | return {value}; 37 | } 38 | 39 | if (value === null) { 40 | return {value}; 41 | } 42 | if (value === undefined) { 43 | return {value}; 44 | } 45 | if (value instanceof Date) { 46 | return {value}; 47 | } 48 | 49 | if (typeof value === 'number' && isNaN(value)) { 50 | return {value}; 51 | } else if (typeof value === 'number' && isFloat(value)) { 52 | return {value}; 53 | } else if (typeof value === 'bigint') { 54 | return {value}; 55 | } else if (typeof value === 'number') { 56 | return {value}; 57 | } 58 | 59 | return null; 60 | }; 61 | Value.displayName = 'JVR.Value'; 62 | -------------------------------------------------------------------------------- /src/external/react-json-view/section/Copied.tsx: -------------------------------------------------------------------------------- 1 | import { type TagType } from '../store/Types.js'; 2 | import { type SectionElement, useSectionStore } from '../store/Section.js'; 3 | import { useSectionRender } from '../utils/useRender.js'; 4 | 5 | export const Copied = (props: SectionElement) => { 6 | const { Copied: Comp = {} } = useSectionStore(); 7 | useSectionRender(Comp, props, 'Copied'); 8 | return null; 9 | }; 10 | 11 | Copied.displayName = 'JVR.Copied'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/section/CountInfo.tsx: -------------------------------------------------------------------------------- 1 | import { type TagType } from "../store/Types.js"; 2 | import { type SectionElement, type SectionElementProps, useSectionStore } from "../store/Section.js"; 3 | import { useSectionRender } from "../utils/useRender.js"; 4 | import { useStore } from "../store.js"; 5 | 6 | export const CountInfo = (props: SectionElement) => { 7 | const { CountInfo: Comp = {} } = useSectionStore(); 8 | useSectionRender(Comp, props, "CountInfo"); 9 | return null; 10 | }; 11 | 12 | CountInfo.displayName = "JVR.CountInfo"; 13 | 14 | interface CountInfoCompProps { 15 | value?: T; 16 | keyName: string | number; 17 | } 18 | 19 | export const CountInfoComp = ( 20 | props: SectionElementProps & CountInfoCompProps & React.HTMLAttributes 21 | ) => { 22 | const { value = {}, keyName, ...other } = props; 23 | const { displayObjectSize } = useStore(); 24 | 25 | const { CountInfo: Comp = {} } = useSectionStore(); 26 | 27 | if (!displayObjectSize) return null; 28 | 29 | const { as, render, ...reset } = Comp; 30 | const Elm = as || "span"; 31 | 32 | reset.style = { ...reset.style, ...props.style }; 33 | 34 | const len = Object.keys(value).length; 35 | if (!reset.children) { 36 | reset.children = `${len} items`; 37 | } 38 | 39 | const elmProps = { ...reset, ...other }; 40 | const isRender = render && typeof render === "function"; 41 | const child = isRender && render({ ...elmProps, "data-length": len }, { value, keyName }); 42 | if (child) return child; 43 | return ; 44 | }; 45 | 46 | CountInfoComp.displayName = "JVR.CountInfoComp"; 47 | -------------------------------------------------------------------------------- /src/external/react-json-view/section/CountInfoExtra.tsx: -------------------------------------------------------------------------------- 1 | import { type TagType } from '../store/Types.js'; 2 | import { type SectionElement, type SectionElementProps, useSectionStore } from '../store/Section.js'; 3 | import { useSectionRender } from '../utils/useRender.js'; 4 | 5 | export const CountInfoExtra = (props: SectionElement) => { 6 | const { CountInfoExtra: Comp = {} } = useSectionStore(); 7 | useSectionRender(Comp, props, 'CountInfoExtra'); 8 | return null; 9 | }; 10 | 11 | CountInfoExtra.displayName = 'JVR.CountInfoExtra'; 12 | 13 | interface CountInfoExtraCompsProps { 14 | value?: T; 15 | keyName: string | number; 16 | } 17 | 18 | export const CountInfoExtraComps = ( 19 | props: SectionElementProps & CountInfoExtraCompsProps, 20 | ) => { 21 | const { value = {}, keyName, ...other } = props; 22 | const { CountInfoExtra: Comp = {} } = useSectionStore(); 23 | const { as, render, ...reset } = Comp; 24 | if (!render && !reset.children) return null; 25 | const Elm = as || 'span'; 26 | const isRender = render && typeof render === 'function'; 27 | const elmProps = { ...reset, ...other }; 28 | const child = isRender && render(elmProps, { value, keyName }); 29 | if (child) return child; 30 | return ; 31 | }; 32 | 33 | CountInfoExtraComps.displayName = 'JVR.CountInfoExtraComps'; 34 | -------------------------------------------------------------------------------- /src/external/react-json-view/section/Ellipsis.tsx: -------------------------------------------------------------------------------- 1 | import { type TagType } from '../store/Types.js'; 2 | import { type SectionElement, useSectionStore } from '../store/Section.js'; 3 | import { useSectionRender } from '../utils/useRender.js'; 4 | 5 | export const Ellipsis = (props: SectionElement) => { 6 | const { Ellipsis: Comp = {} } = useSectionStore(); 7 | useSectionRender(Comp, props, 'Ellipsis'); 8 | return null; 9 | }; 10 | 11 | Ellipsis.displayName = 'JVR.Ellipsis'; 12 | 13 | interface EllipsisCompProps { 14 | value?: T; 15 | keyName: string | number; 16 | isExpanded: boolean; 17 | } 18 | 19 | export const EllipsisComp = ({ isExpanded, value, keyName }: EllipsisCompProps) => { 20 | const { Ellipsis: Comp = {} } = useSectionStore(); 21 | const { as, render, ...reset } = Comp; 22 | const Elm = as || 'span'; 23 | const child = 24 | render && typeof render === 'function' && render({ ...reset, 'data-expanded': isExpanded }, { value, keyName }); 25 | if (child) return child; 26 | if (!isExpanded) return null; 27 | return ; 28 | }; 29 | 30 | EllipsisComp.displayName = 'JVR.EllipsisComp'; 31 | -------------------------------------------------------------------------------- /src/external/react-json-view/section/KeyName.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, type PropsWithChildren } from 'react'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { type SectionElement, useSectionStore } from '../store/Section.js'; 4 | import { useSectionRender } from '../utils/useRender.js'; 5 | 6 | export const KeyName = (props: SectionElement) => { 7 | const { KeyName: Comp = {} } = useSectionStore(); 8 | useSectionRender(Comp, props, 'KeyName'); 9 | return null; 10 | }; 11 | 12 | KeyName.displayName = 'JVR.KeyName'; 13 | 14 | type KeyNameCompProps = { 15 | keyName: string | number; 16 | value?: unknown; 17 | }; 18 | 19 | export const KeyNameComp: FC> = (props) => { 20 | const { children, value, keyName } = props; 21 | const isNumber = typeof children === 'number'; 22 | const style: React.CSSProperties = { 23 | color: isNumber ? 'var(--w-rjv-key-number, #268bd2)' : 'var(--w-rjv-key-string, #002b36)', 24 | }; 25 | const { KeyName: Comp = {} } = useSectionStore(); 26 | const { as, render, ...reset } = Comp; 27 | reset.style = { ...reset.style, ...style }; 28 | const Elm = as || 'span'; 29 | const child = render && typeof render === 'function' && render({ ...reset, children }, { value, keyName }); 30 | if (child) return child; 31 | return {children}; 32 | }; 33 | 34 | KeyNameComp.displayName = 'JVR.KeyNameComp'; 35 | -------------------------------------------------------------------------------- /src/external/react-json-view/store/Expands.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, createContext, useContext, useReducer } from 'react'; 2 | 3 | type InitialState = { 4 | [key: string]: boolean; 5 | }; 6 | 7 | type Dispatch = React.Dispatch; 8 | 9 | const initialState: InitialState = {}; 10 | const Context = createContext(initialState); 11 | 12 | const reducer = (state: InitialState, action: InitialState) => ({ 13 | ...state, 14 | ...action, 15 | }); 16 | 17 | export const useExpandsStore = () => { 18 | return useContext(Context); 19 | }; 20 | 21 | const DispatchExpands = createContext(() => {}); 22 | DispatchExpands.displayName = 'JVR.DispatchExpands'; 23 | 24 | export function useExpands() { 25 | return useReducer(reducer, initialState); 26 | } 27 | 28 | export function useExpandsDispatch() { 29 | return useContext(DispatchExpands); 30 | } 31 | 32 | interface ExpandsProps { 33 | initial: InitialState; 34 | dispatch: Dispatch; 35 | } 36 | 37 | export const Expands: FC> = ({ initial, dispatch, children }) => { 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | }; 44 | Expands.displayName = 'JVR.Expands'; 45 | -------------------------------------------------------------------------------- /src/external/react-json-view/store/ShowTools.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, createContext, useContext, useReducer } from 'react'; 2 | 3 | type InitialState = Record; 4 | type Dispatch = React.Dispatch; 5 | 6 | const initialState: InitialState = {}; 7 | const Context = createContext(initialState); 8 | 9 | const reducer = (state: InitialState, action: InitialState) => ({ 10 | ...state, 11 | ...action, 12 | }); 13 | 14 | export const useShowToolsStore = () => { 15 | return useContext(Context); 16 | }; 17 | 18 | const DispatchShowTools = createContext(() => {}); 19 | DispatchShowTools.displayName = 'JVR.DispatchShowTools'; 20 | 21 | export function useShowTools() { 22 | return useReducer(reducer, initialState); 23 | } 24 | 25 | export function useShowToolsDispatch() { 26 | return useContext(DispatchShowTools); 27 | } 28 | 29 | interface ShowToolsProps { 30 | initial: InitialState; 31 | dispatch: Dispatch; 32 | } 33 | 34 | export const ShowTools: FC> = ({ initial, dispatch, children }) => { 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; 41 | ShowTools.displayName = 'JVR.ShowTools'; 42 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const Arrow = (props: SymbolsElement) => { 6 | const { Arrow: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'Arrow'); 8 | return null; 9 | }; 10 | 11 | Arrow.displayName = 'JVR.Arrow'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/BraceLeft.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const BraceLeft = (props: SymbolsElement) => { 6 | const { BraceLeft: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'BraceLeft'); 8 | 9 | return null; 10 | }; 11 | 12 | BraceLeft.displayName = 'JVR.BraceLeft'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/BraceRight.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const BraceRight = (props: SymbolsElement) => { 6 | const { BraceRight: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'BraceRight'); 8 | return null; 9 | }; 10 | 11 | BraceRight.displayName = 'JVR.BraceRight'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/BracketsLeft.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const BracketsLeft = (props: SymbolsElement) => { 6 | const { BracketsLeft: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'BracketsLeft'); 8 | 9 | return null; 10 | }; 11 | 12 | BracketsLeft.displayName = 'JVR.BracketsLeft'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/BracketsRight.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const BracketsRight = (props: SymbolsElement) => { 6 | const { BracketsRight: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'BracketsRight'); 8 | 9 | return null; 10 | }; 11 | 12 | BracketsRight.displayName = 'JVR.BracketsRight'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/Colon.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const Colon = (props: SymbolsElement) => { 6 | const { Colon: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'Colon'); 8 | 9 | return null; 10 | }; 11 | 12 | Colon.displayName = 'JVR.Colon'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/Quote.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const Quote = (props: SymbolsElement) => { 6 | const { Quote: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'Quote'); 8 | 9 | return null; 10 | }; 11 | 12 | Quote.displayName = 'JVR.Quote'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/symbol/ValueQuote.tsx: -------------------------------------------------------------------------------- 1 | import { useSymbolsStore, type SymbolsElement } from '../store/Symbols.js'; 2 | import { type TagType } from '../store/Types.js'; 3 | import { useSymbolsRender } from '../utils/useRender.js'; 4 | 5 | export const ValueQuote = (props: SymbolsElement) => { 6 | const { ValueQuote: Comp = {} } = useSymbolsStore(); 7 | useSymbolsRender(Comp, props, 'ValueQuote'); 8 | 9 | return null; 10 | }; 11 | 12 | ValueQuote.displayName = 'JVR.ValueQuote'; 13 | -------------------------------------------------------------------------------- /src/external/react-json-view/theme/custom.tsx: -------------------------------------------------------------------------------- 1 | export const customTheme = { 2 | "--w-rjv-font-family": "monospace", 3 | "--w-rjv-color": "#E7E5E4", 4 | "--w-rjv-key-string": "#fff", 5 | "--w-rjv-background-color": "#2e3440", 6 | "--w-rjv-line-color": "#4c566a", 7 | "--w-rjv-arrow-color": "var(--w-rjv-color)", 8 | "--w-rjv-edit-color": "var(--w-rjv-color)", 9 | "--w-rjv-info-color": "#60A5FA", 10 | "--w-rjv-update-color": "#88c0cf75", 11 | "--w-rjv-copied-color": "#119cc0", 12 | "--w-rjv-copied-success-color": "#28a745", 13 | "--w-rjv-curlybraces-color": "#E7E5E4", 14 | "--w-rjv-colon-color": "#E7E5E4", 15 | "--w-rjv-brackets-color": "#E7E5E4", 16 | "--w-rjv-quotes-color": "var(--w-rjv-key-string)", 17 | "--w-rjv-quotes-string-color": "var(--w-rjv-type-string-color)", 18 | "--w-rjv-ellipsis-color": "var(--w-rjv-color)", 19 | "--w-rjv-type-string-color": "#28a745", 20 | "--w-rjv-type-int-color": "#60A5FA", 21 | "--w-rjv-type-float-color": "#60A5FA", 22 | "--w-rjv-type-bigint-color": "#60A5FA", 23 | "--w-rjv-type-boolean-color": "#F43F5E", 24 | "--w-rjv-type-date-color": "#41a2c2", 25 | "--w-rjv-type-url-color": "#5e81ac", 26 | "--w-rjv-type-null-color": "#22D3EE", 27 | "--w-rjv-type-nan-color": "#60A5FA", 28 | "--w-rjv-type-undefined-color": "#22D3EE", 29 | } as React.CSSProperties; 30 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Bigint.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Bigint = (props: TypesElement) => { 5 | const { Bigint: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Bigint'); 7 | 8 | return null; 9 | }; 10 | 11 | Bigint.displayName = 'JVR.Bigint'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Date.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Date = (props: TypesElement) => { 5 | const { Date: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Date'); 7 | 8 | return null; 9 | }; 10 | 11 | Date.displayName = 'JVR.Date'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/False.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const False = (props: TypesElement) => { 5 | const { False: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'False'); 7 | 8 | return null; 9 | }; 10 | 11 | False.displayName = 'JVR.False'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Float.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Float = (props: TypesElement) => { 5 | const { Float: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Float'); 7 | 8 | return null; 9 | }; 10 | 11 | Float.displayName = 'JVR.Float'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Int.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Int = (props: TypesElement) => { 5 | const { Int: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Int'); 7 | 8 | return null; 9 | }; 10 | 11 | Int.displayName = 'JVR.Int'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Map.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Map = (props: TypesElement) => { 5 | const { Map: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Map'); 7 | 8 | return null; 9 | }; 10 | 11 | Map.displayName = 'JVR.Map'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Nan.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Nan = (props: TypesElement) => { 5 | const { Nan: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Nan'); 7 | 8 | return null; 9 | }; 10 | 11 | Nan.displayName = 'JVR.Nan'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Null.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Null = (props: TypesElement) => { 5 | const { Null: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Null'); 7 | 8 | return null; 9 | }; 10 | 11 | Null.displayName = 'JVR.Null'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Set.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Set = (props: TypesElement) => { 5 | const { Set: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Set'); 7 | 8 | return null; 9 | }; 10 | 11 | Set.displayName = 'JVR.Set'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/String.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const StringText = (props: TypesElement) => { 5 | const { Str: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Str'); 7 | 8 | return null; 9 | }; 10 | 11 | StringText.displayName = 'JVR.StringText'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/True.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const True = (props: TypesElement) => { 5 | const { True: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'True'); 7 | 8 | return null; 9 | }; 10 | 11 | True.displayName = 'JVR.True'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Undefined.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Undefined = (props: TypesElement) => { 5 | const { Undefined: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Undefined'); 7 | 8 | return null; 9 | }; 10 | 11 | Undefined.displayName = 'JVR.Undefined'; 12 | -------------------------------------------------------------------------------- /src/external/react-json-view/types/Url.tsx: -------------------------------------------------------------------------------- 1 | import { useTypesStore, type TagType, type TypesElement } from '../store/Types.js'; 2 | import { useTypesRender } from '../utils/useRender.js'; 3 | 4 | export const Url = (props: TypesElement) => { 5 | const { Url: Comp = {} } = useTypesStore(); 6 | useTypesRender(Comp, props, 'Url'); 7 | 8 | return null; 9 | }; 10 | 11 | Url.displayName = 'JVR.Url'; 12 | -------------------------------------------------------------------------------- /src/gradients.css: -------------------------------------------------------------------------------- 1 | .sea-gradient { 2 | background-color: #bbf7d0; /* Tailwind's bg-green-100 */ 3 | background-image: linear-gradient(to right, rgba(6, 182, 212, 0.5), rgba(59, 130, 246, 0.5)); 4 | } 5 | 6 | .hyper-gradient { 7 | background-image: linear-gradient(to right, #ec4899, #ef4444, #f59e0b); 8 | } 9 | 10 | .gotham-gradient { 11 | background-image: linear-gradient(to right, #374151, #111827, #000000); 12 | } 13 | 14 | .gray-gradient { 15 | background-image: linear-gradient(to right, rgba(55, 65, 81, 0.5), rgba(17, 24, 39, 0.5), rgba(0, 0, 0, 0.5)); 16 | } 17 | 18 | .watermelon-gradient { 19 | background-image: linear-gradient(to right, #ef4444, #10b981); 20 | } 21 | 22 | .ice-gradient { 23 | background-image: linear-gradient(to right, #fda4af, #5eead4); 24 | } 25 | 26 | .silver-gradient { 27 | background-image: linear-gradient(to right, #f3f4f6, #d1d5db); 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Tab } from "./client/tabs/index.js" 2 | import type { ExtendedContext } from "./context/extend-context.js" 3 | 4 | export { reactRouterDevTools, defineRdtConfig } from "./vite/plugin.js" 5 | // Type exports 6 | export type { EmbeddedDevToolsProps } from "./client/embedded-dev-tools.js" 7 | export type { ReactRouterDevtoolsProps as ReactRouterToolsProps } from "./client/react-router-dev-tools.js" 8 | export type { ExtendedContext } from "./context/extend-context.js" 9 | 10 | export type RdtPlugin = (...args: any) => Tab 11 | 12 | declare module "react-router" { 13 | interface LoaderFunctionArgs { 14 | devTools?: ExtendedContext 15 | } 16 | interface ActionFunctionArgs { 17 | devTools?: ExtendedContext 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | export { defineServerConfig } from "./server/config.js" 2 | export { withLoaderWrapper, withActionWrapper } from "./server/hof.js" 3 | -------------------------------------------------------------------------------- /src/server/config.test.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "./config" 2 | 3 | describe("getConfig Test suite", () => { 4 | it("should return the server config when set on the process object", () => { 5 | process.rdt_config = { 6 | silent: true, 7 | } 8 | expect(getConfig()).toEqual({ 9 | silent: true, 10 | }) 11 | }) 12 | 13 | it("should return an empty object if the config is not set on the process object", () => { 14 | process.rdt_config = {} 15 | expect(getConfig()).toEqual({}) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/server/config.ts: -------------------------------------------------------------------------------- 1 | export interface DevToolsServerConfig { 2 | /** 3 | * Whether to log in the console, this turns off ALL logging 4 | * If you want to be granulat use the logs option 5 | * @default true 6 | */ 7 | silent?: boolean 8 | /** 9 | * The threshold for server timings to be logged in the console 10 | * If the server timing is greater than this threshold, it will be logged in red, otherwise it will be logged in green 11 | * @default Number.POSITIVE_INFINITY 12 | * 13 | */ 14 | serverTimingThreshold?: number 15 | logs?: { 16 | /** 17 | * Whether to log cookie headers in the console 18 | * @default true 19 | */ 20 | cookies?: boolean 21 | /** 22 | * Whether to log deferred loaders in the console 23 | * @default true 24 | */ 25 | defer?: boolean 26 | /** 27 | * Whether to log action calls in the console 28 | * @default true 29 | * */ 30 | actions?: boolean 31 | /** 32 | * Whether to log loader calls in the console 33 | * @default true 34 | */ 35 | loaders?: boolean 36 | /** 37 | * Whether to log cache headers in the console 38 | * @default true 39 | */ 40 | cache?: boolean 41 | /** 42 | * Whether to log site clear headers in the console 43 | * @default true 44 | */ 45 | siteClear?: boolean 46 | /** 47 | * Whether to log server timings headers in the console 48 | * @default true 49 | */ 50 | serverTimings?: boolean 51 | } 52 | } 53 | declare global { 54 | namespace NodeJS { 55 | interface Process { 56 | rdt_config: DevToolsServerConfig 57 | rdt_port: number 58 | } 59 | } 60 | } 61 | export const defineServerConfig = (config: DevToolsServerConfig) => config 62 | 63 | export const getConfig = () => process.rdt_config ?? { silent: true } 64 | -------------------------------------------------------------------------------- /src/server/event-queue.ts: -------------------------------------------------------------------------------- 1 | interface RDTEvent | any[]> { 2 | type: Type 3 | data: Data 4 | } 5 | 6 | export type LoaderEvent = RDTEvent< 7 | "loader", 8 | { 9 | id: string 10 | executionTime: number 11 | requestData: any 12 | responseData: any 13 | requestHeaders: Record 14 | responseHeaders: Record 15 | timestamp: number 16 | } 17 | > 18 | export type ActionEvent = RDTEvent< 19 | "action", 20 | { 21 | id: string 22 | executionTime: number 23 | requestData: any 24 | responseData: any 25 | requestHeaders: Record 26 | responseHeaders: Record 27 | timestamp: number 28 | } 29 | > 30 | -------------------------------------------------------------------------------- /src/server/hof.ts: -------------------------------------------------------------------------------- 1 | import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router" 2 | import { analyzeLoaderOrAction } from "./utils" 3 | 4 | export const withLoaderWrapper = (loader: (args: LoaderFunctionArgs) => Promise, id: string) => { 5 | return analyzeLoaderOrAction(id, "loader", loader) 6 | } 7 | 8 | export const withActionWrapper = (action: (args: ActionFunctionArgs) => Promise, id: string) => { 9 | return analyzeLoaderOrAction(id, "action", action) 10 | } 11 | -------------------------------------------------------------------------------- /src/server/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { getConfig } from "./config.js" 3 | 4 | const log = (message: string) => { 5 | const config = getConfig() 6 | if (config.silent) { 7 | return 8 | } 9 | 10 | // biome-ignore lint/suspicious/noConsole: disable noConsole rule for this line 11 | console.log(message) 12 | } 13 | 14 | export const errorLog = (message: string) => { 15 | log(`${chalk.redBright.bold("ERROR")} ${message}`) 16 | } 17 | 18 | export const redirectLog = (message: string) => { 19 | log(`${chalk.yellowBright.bold("REDIRECT")} ${message}`) 20 | } 21 | 22 | export const infoLog = (message: string) => { 23 | log(`${chalk.blueBright.bold("INFO")} ${message}`) 24 | } 25 | 26 | export const loaderLog = (message: string) => { 27 | const config = getConfig() 28 | if (config.logs?.loaders === false) { 29 | return 30 | } 31 | const messageToLog = `${chalk.green.bold("LOADER")} ${message}` 32 | log(messageToLog) 33 | return messageToLog 34 | } 35 | 36 | export const actionLog = (message: string) => { 37 | const config = getConfig() 38 | if (config.logs?.actions === false) { 39 | return 40 | } 41 | const messageToLog = `${chalk.yellowBright.bold("ACTION")} ${message}` 42 | log(messageToLog) 43 | return messageToLog 44 | } 45 | 46 | // const successLog = (message: string) => { 47 | // log(`${chalk.greenBright.bold("SUCCESS")} ${message}`); 48 | //}; 49 | 50 | //const warningLog = (message: string) => { 51 | //log(`${chalk.bgYellow(logPrefix("WARNING"))} ${message}`); 52 | //}; 53 | 54 | // const debugLog = (message: string) => { 55 | // log(`${chalk.bgMagenta(logPrefix("DEBUG"))} ${message}`); 56 | //}; 57 | -------------------------------------------------------------------------------- /src/server/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { parseCacheControlHeader } from "./parser" 2 | 3 | describe("parseCacheControlHeader Test suite", () => { 4 | it("should return an object with the expected keys and values", () => { 5 | const header = "max-age=3600, s-maxage=600, private" 6 | const result = parseCacheControlHeader(new Headers({ "Cache-Control": header })) 7 | expect(result).toEqual({ 8 | maxAge: "3600", 9 | sMaxage: "600", 10 | private: true, 11 | }) 12 | }) 13 | 14 | it("should return an empty object if the cache header is an empty object", () => { 15 | const header = "" 16 | const result = parseCacheControlHeader(new Headers({ "Cache-Control": header })) 17 | expect(result).toEqual({}) 18 | }) 19 | 20 | it("should return an empty object if the cache header is not present", () => { 21 | const result = parseCacheControlHeader(new Headers()) 22 | expect(result).toEqual({}) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/server/parser.ts: -------------------------------------------------------------------------------- 1 | import { uppercaseFirstLetter } from "../client/utils/sanitize.js" 2 | 3 | export interface CacheControl { 4 | maxAge?: string 5 | maxStale?: string 6 | minFresh?: string 7 | sMaxage?: string 8 | noCache?: boolean 9 | noStore?: boolean 10 | noTransform?: boolean 11 | onlyIfCached?: boolean 12 | public?: boolean 13 | private?: boolean 14 | mustRevalidate?: boolean 15 | proxyRevalidate?: boolean 16 | mustUnderstand?: boolean 17 | immutable?: boolean 18 | staleWhileRevalidate?: string 19 | staleIfError?: string 20 | } 21 | 22 | export const parseCacheControlHeader = (headers: Headers) => { 23 | const cacheControl = headers.get("cache-control") 24 | if (!cacheControl) return {} 25 | const parts = cacheControl.split(",") 26 | const cacheControlObject: Record = {} 27 | for (const part of parts) { 28 | const [key, value] = part.split("=") 29 | if (!key) continue 30 | cacheControlObject[key.trim()] = value?.trim() 31 | } 32 | 33 | const returnValue = Object.entries(cacheControlObject).reduce((acc, [key, value]) => { 34 | const k = key 35 | .trim() 36 | .split("-") 37 | .map((k, i) => (i === 0 ? k : uppercaseFirstLetter(k))) 38 | .join("") 39 | if (!value) { 40 | // biome-ignore lint/performance/noAccumulatingSpread: 41 | return { ...acc, [k]: true } 42 | } 43 | // biome-ignore lint/performance/noAccumulatingSpread: 44 | return { ...acc, [k]: value } 45 | }, {} as CacheControl) 46 | 47 | return returnValue 48 | } 49 | -------------------------------------------------------------------------------- /src/server/perf.test.ts: -------------------------------------------------------------------------------- 1 | import { diffInMs, secondsToHuman } from "./perf" 2 | 3 | describe("diffInMs", () => { 4 | it("should return the difference in milliseconds between two dates", () => { 5 | const start = Date.now() 6 | const end = Date.now() + 1000 7 | expect(diffInMs(start, end)).toBe(1000) 8 | }) 9 | }) 10 | describe("secondsToHuman", () => { 11 | it("should return the number of seconds as a human readable string", () => { 12 | // 1 second 13 | expect(secondsToHuman(1)).toBe("1s") 14 | // 1 minute 15 | expect(secondsToHuman(60)).toBe("1m") 16 | // 1 minute and 10 seconds 17 | expect(secondsToHuman(60 + 10)).toBe("1:10m") 18 | // 5 minutes 19 | expect(secondsToHuman(5 * 60)).toBe("5m") 20 | // 5 minutes and 2 seconds 21 | expect(secondsToHuman(5 * 60 + 2)).toBe("5:02m") 22 | // 5 minutes and 15 seconds 23 | expect(secondsToHuman(5 * 60 + 15)).toBe("5:15m") 24 | // 1 hour 25 | expect(secondsToHuman(60 * 60)).toBe("1h") 26 | // 1 hour and 1 minute 27 | expect(secondsToHuman(60 * 60 + 60)).toBe("1:01:00h") 28 | // 1 hour and 1 minute and 10 seconds 29 | expect(secondsToHuman(60 * 60 + 60 + 10)).toBe("1:01:10h") 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/server/perf.ts: -------------------------------------------------------------------------------- 1 | export const diffInMs = (start: number, end: number = performance.now()) => Number((end - start).toFixed(2)) 2 | 3 | export const secondsToHuman = (s: number) => { 4 | if (s > 3600) { 5 | const hours = Math.floor(s / 3600) 6 | const minutes = Math.floor((s % 3600) / 60) 7 | const seconds = Math.floor((s % 3600) % 60) 8 | const minutesString = minutes < 10 ? `0${minutes}` : minutes 9 | const secondsString = seconds < 10 ? `0${seconds}` : seconds 10 | 11 | if (minutes === 0 && seconds === 0) return `${hours}h` 12 | if (seconds === 0) return `${hours}:${minutesString}:${secondsString}h` 13 | return `${hours}:${minutesString}:${secondsString}h` 14 | } 15 | if (s > 60) { 16 | const minutes = Math.floor(s / 60) 17 | const seconds = Math.floor(s % 60) 18 | const secondsString = seconds < 10 ? `0${seconds}` : seconds 19 | if (seconds === 0 && minutes === 60) return "1h" 20 | if (seconds === 0) return `${minutes}m` 21 | return `${minutes}:${secondsString}m` 22 | } 23 | if (s === 60) return "1m" 24 | return `${s}s` 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/bigint-util.ts: -------------------------------------------------------------------------------- 1 | export const bigIntReplacer = (key: any, value: any) => (typeof value === "bigint" ? value.toString() : value) 2 | 3 | export const convertBigIntToString = (data: any): any => { 4 | if (typeof data === "bigint") { 5 | return data.toString() 6 | } 7 | 8 | if (Array.isArray(data)) { 9 | return data.map((item) => convertBigIntToString(item)) 10 | } 11 | 12 | if (data !== null && typeof data === "object") { 13 | return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, convertBigIntToString(value)])) 14 | } 15 | 16 | return data 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/request-event.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunctionArgs, 3 | ClientActionFunctionArgs, 4 | ClientLoaderFunctionArgs, 5 | LoaderFunctionArgs, 6 | } from "react-router" 7 | 8 | export type AllDataFunctionArgs = 9 | | LoaderFunctionArgs 10 | | ActionFunctionArgs 11 | | ClientLoaderFunctionArgs 12 | | ClientActionFunctionArgs 13 | export type NetworkRequestType = "action" | "loader" | "client-action" | "client-loader" 14 | type NetworkRequestTypeFull = "action" | "loader" | "client-action" | "client-loader" | "custom-event" 15 | 16 | export type RequestEvent = { 17 | routine?: "request-event" 18 | type: NetworkRequestTypeFull 19 | headers: Record 20 | id: string 21 | startTime: number 22 | endTime?: number | undefined 23 | data?: any | undefined 24 | method: string 25 | status?: string 26 | url: string 27 | aborted?: boolean 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/send-event.ts: -------------------------------------------------------------------------------- 1 | import { bigIntReplacer } from "./bigint-util" 2 | import type { RequestEvent } from "./request-event" 3 | 4 | export const sendEvent = (event: RequestEvent) => { 5 | if (typeof process === "undefined") { 6 | return 7 | } 8 | const port = process.rdt_port 9 | 10 | if (port) { 11 | fetch(`http://localhost:${port}/__rrdt`, { 12 | method: "POST", 13 | body: JSON.stringify({ routine: "request-event", ...event }, bigIntReplacer), 14 | }) 15 | .then(async (res) => { 16 | // avoid memory leaks 17 | if (res.ok) { 18 | await res.text() 19 | } 20 | }) 21 | .catch(() => {}) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /src/vite/generators/action.ts: -------------------------------------------------------------------------------- 1 | export const generateAction = () => { 2 | return ["export const action = async ({ request }: ActionFunctionArgs) => {", " return null;", "};"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/clientAction.ts: -------------------------------------------------------------------------------- 1 | export const generateClientAction = () => { 2 | return [ 3 | "export const clientAction = async ({ request }: ClientActionFunctionArgs) => {", 4 | " return null;", 5 | "};", 6 | ].join("\n") 7 | } 8 | -------------------------------------------------------------------------------- /src/vite/generators/clintLoader.ts: -------------------------------------------------------------------------------- 1 | export const generateClientLoader = () => { 2 | return [ 3 | "export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {", 4 | " return null;", 5 | "};", 6 | ].join("\n") 7 | } 8 | -------------------------------------------------------------------------------- /src/vite/generators/component.ts: -------------------------------------------------------------------------------- 1 | export const generateComponent = (withLoader = false) => { 2 | return [ 3 | "export default function RouteComponent(){", 4 | ...(withLoader ? [" const data = useLoaderData()"] : []), 5 | " return (", 6 | "
", 7 | " );", 8 | "}", 9 | ].join("\n") 10 | } 11 | -------------------------------------------------------------------------------- /src/vite/generators/dependencies.ts: -------------------------------------------------------------------------------- 1 | export const generateDependencies = (selectedGenerators: string[], shouldIncludeLoaderData = true) => { 2 | const actionImports = "" 3 | const loaderImports = "" 4 | const reactRouterDeps = [] 5 | const typeDeps = [] 6 | 7 | if (selectedGenerators.includes("meta")) { 8 | typeDeps.push("MetaFunction") 9 | } 10 | if (selectedGenerators.includes("headers")) { 11 | typeDeps.push("HeadersFunction") 12 | } 13 | if (selectedGenerators.includes("links")) { 14 | typeDeps.push("LinksFunction") 15 | } 16 | if (selectedGenerators.includes("loader")) { 17 | typeDeps.push("LoaderFunctionArgs") 18 | if (shouldIncludeLoaderData) { 19 | reactRouterDeps.push("useLoaderData") 20 | } 21 | } 22 | if (selectedGenerators.includes("clientLoader")) { 23 | typeDeps.push("ClientLoaderFunctionArgs") 24 | } 25 | if (selectedGenerators.includes("clientAction")) { 26 | typeDeps.push("ClientActionFunctionArgs") 27 | } 28 | if (selectedGenerators.includes("action")) { 29 | typeDeps.push("ActionFunctionArgs") 30 | } 31 | if (selectedGenerators.includes("revalidate")) { 32 | typeDeps.push("ShouldRevalidateFunction") 33 | } 34 | if (selectedGenerators.includes("errorBoundary")) { 35 | reactRouterDeps.push("isRouteErrorResponse", "useRouteError") 36 | } 37 | 38 | const output: string[] = [ 39 | ...(actionImports && selectedGenerators.includes("action") ? [actionImports] : []), 40 | ...(loaderImports && selectedGenerators.includes("loader") ? [loaderImports] : []), 41 | ] 42 | 43 | if (typeDeps.length) { 44 | output.push(`import type { ${typeDeps.join(", ")} } from "react-router";`) 45 | } 46 | if (reactRouterDeps.length) { 47 | output.push(`import { ${reactRouterDeps.join(", ")} } from "react-router";`) 48 | } 49 | 50 | return output.join("\n") 51 | } 52 | -------------------------------------------------------------------------------- /src/vite/generators/errorBoundary.ts: -------------------------------------------------------------------------------- 1 | export const generateErrorBoundary = () => { 2 | return [ 3 | "export function ErrorBoundary(){", 4 | " const error = useRouteError();", 5 | " if (isRouteErrorResponse(error)) {", 6 | " return
", 7 | " }", 8 | " return
", 9 | "}", 10 | ].join("\n") 11 | } 12 | -------------------------------------------------------------------------------- /src/vite/generators/handler.ts: -------------------------------------------------------------------------------- 1 | export const generateHandler = () => { 2 | return ["export const handle = () => ({", " // your handler here", "});"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/headers.ts: -------------------------------------------------------------------------------- 1 | export const generateHeaders = () => { 2 | return ["export const headers: HeadersFunction = () => (", " {", " // your headers here", " }", ");"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/index.ts: -------------------------------------------------------------------------------- 1 | import { generateAction } from "./action" 2 | import { generateClientAction } from "./clientAction" 3 | import { generateClientLoader } from "./clintLoader" 4 | import { generateComponent } from "./component" 5 | import { generateDependencies } from "./dependencies" 6 | import { generateErrorBoundary } from "./errorBoundary" 7 | import { generateHandler } from "./handler" 8 | import { generateHeaders } from "./headers" 9 | import { generateLinks } from "./links" 10 | import { generateLoader } from "./loader" 11 | import { generateMeta } from "./meta" 12 | import { generateRevalidate } from "./revalidate" 13 | 14 | export const GENERATORS = { 15 | action: generateAction, 16 | component: generateComponent, 17 | errorBoundary: generateErrorBoundary, 18 | handler: generateHandler, 19 | headers: generateHeaders, 20 | links: generateLinks, 21 | loader: generateLoader, 22 | clientLoader: generateClientLoader, 23 | clientAction: generateClientAction, 24 | meta: generateMeta, 25 | revalidate: generateRevalidate, 26 | dependencies: generateDependencies, 27 | } as const 28 | 29 | export type Generator = keyof typeof GENERATORS 30 | -------------------------------------------------------------------------------- /src/vite/generators/links.ts: -------------------------------------------------------------------------------- 1 | export const generateLinks = () => { 2 | return ["export const links: LinksFunction = () => (", " [", " // your links here", " ]", ");"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/loader.ts: -------------------------------------------------------------------------------- 1 | export const generateLoader = () => { 2 | return ["export const loader = async ({ request }: LoaderFunctionArgs) => {", " return null;", "};"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/meta.ts: -------------------------------------------------------------------------------- 1 | export const generateMeta = () => { 2 | return ["export const meta: MetaFunction = () => [", " // your meta here", "];"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/generators/revalidate.ts: -------------------------------------------------------------------------------- 1 | export const generateRevalidate = () => { 2 | return ["export const shouldRevalidate: ShouldRevalidateFunction = () => {", " return true;", "};"].join("\n") 3 | } 4 | -------------------------------------------------------------------------------- /src/vite/node-server.ts: -------------------------------------------------------------------------------- 1 | import { createServer, version as viteVersion } from "vite" 2 | import { ViteNodeRunner } from "vite-node/client" 3 | import { ViteNodeServer } from "vite-node/server" 4 | import { installSourcemapsSupport } from "vite-node/source-map" 5 | 6 | // create vite server 7 | const server = await createServer({ 8 | mode: "development", 9 | root: process.cwd(), 10 | server: { 11 | preTransformRequests: false, 12 | hmr: false, 13 | watch: null, 14 | }, 15 | 16 | optimizeDeps: { 17 | noDiscovery: true, 18 | }, 19 | configFile: false, 20 | envFile: false, 21 | plugins: [], 22 | }) 23 | // For old Vite, this is need to initialize the plugins. 24 | if (Number(viteVersion.split(".")[0]) < 6) { 25 | await server.pluginContainer.buildStart({}) 26 | } 27 | 28 | // create vite-node server 29 | const node = new ViteNodeServer(server) 30 | 31 | // fixes stacktraces in Errors 32 | installSourcemapsSupport({ 33 | getSourceMap: (source) => node.getSourceMap(source), 34 | }) 35 | 36 | // create vite-node runner 37 | const runner = new ViteNodeRunner({ 38 | root: server.config.root, 39 | base: server.config.base, 40 | // when having the server and runner in a different context, 41 | // you will need to handle the communication between them 42 | // and pass to this function 43 | fetchModule(id) { 44 | return node.fetchModule(id) 45 | }, 46 | resolveId(id, importer) { 47 | return node.resolveId(id, importer) 48 | }, 49 | }) 50 | 51 | export { runner } 52 | -------------------------------------------------------------------------------- /src/vite/utils/babel.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "@babel/parser" 2 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 3 | import type { NodePath } from "@babel/traverse" 4 | 5 | import * as t from "@babel/types" 6 | 7 | export { parse, t } 8 | import generate from "@babel/generator" 9 | import traverse from "@babel/traverse" 10 | export const trav = 11 | typeof (traverse as any).default !== "undefined" 12 | ? ((traverse as any).default as typeof import("@babel/traverse").default) 13 | : traverse 14 | 15 | export const gen = 16 | typeof (generate as any).default !== "undefined" 17 | ? ((generate as any).default as typeof import("@babel/generator").default) 18 | : generate 19 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{tsx,ts,js}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | popover: "#212121", 8 | main: "#212121", 9 | "popover-foreground": "#fff", 10 | }, 11 | keyframes: { 12 | "accordion-down": { 13 | from: { height: 0 }, 14 | to: { height: "var(--radix-accordion-content-height)" }, 15 | }, 16 | "accordion-up": { 17 | from: { height: "var(--radix-accordion-content-height)" }, 18 | to: { height: 0 }, 19 | }, 20 | "fade-in-left": { 21 | "0%": { opacity: 0, transform: "translateX(-100%)" }, 22 | "100%": { opacity: 1, transform: "translateX(0)" }, 23 | }, 24 | "fade-in": { 25 | "0%": { opacity: 0 }, 26 | "100%": { opacity: 1 }, 27 | }, 28 | }, 29 | animation: { 30 | "accordion-down": "accordion-down 0.2s ease-out", 31 | "accordion-up": "accordion-up 0.2s ease-out", 32 | "fade-in-left": "fade-in-left 0.5s ease-in", 33 | "fade-in": "fade-in 0.5s ease-in", 34 | }, 35 | }, 36 | }, 37 | plugins: [await import("tailwindcss-animate")], 38 | } 39 | -------------------------------------------------------------------------------- /test-apps/custom-server/.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /test-apps/custom-server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | *.tsbuildinfo 4 | 5 | # React Router 6 | /.react-router/ 7 | /build/ 8 | -------------------------------------------------------------------------------- /test-apps/custom-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json server.js /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /test-apps/custom-server/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test-apps/custom-server/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /test-apps/custom-server/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader({ context }: Route.LoaderArgs) { 12 | return { message: context.VALUE_FROM_EXPRESS }; 13 | } 14 | 15 | export default function Home({ loaderData }: Route.ComponentProps) { 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /test-apps/custom-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-server", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "react-router build", 7 | "dev": "cross-env NODE_ENV=development node server.js", 8 | "start": "node server.js", 9 | "typecheck": "react-router typegen && tsc -b" 10 | }, 11 | "dependencies": { 12 | "@react-router/express": "^7.5.3", 13 | "@react-router/node": "^7.5.3", 14 | "compression": "^1.7.5", 15 | "express": "^5.1.0", 16 | "isbot": "^5.1.27", 17 | "morgan": "^1.10.0", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "react-router": "^7.5.3" 21 | }, 22 | "devDependencies": { 23 | "@react-router/dev": "^7.5.3", 24 | "@tailwindcss/vite": "^4.1.4", 25 | "@types/compression": "^1.7.5", 26 | "@types/express": "^5.0.1", 27 | "@types/express-serve-static-core": "^5.0.6", 28 | "@types/morgan": "^1.9.9", 29 | "@types/node": "^20", 30 | "@types/react": "^19.1.2", 31 | "@types/react-dom": "^19.1.2", 32 | "cross-env": "^7.0.3", 33 | "tailwindcss": "^4.1.4", 34 | "typescript": "^5.8.3", 35 | "vite": "^6.3.3", 36 | "vite-tsconfig-paths": "^5.1.4", 37 | "react-router-devtools": "*", 38 | "@tailwindcss/postcss": "^4.1.5", 39 | "postcss-import": "16.1.0" 40 | } 41 | } -------------------------------------------------------------------------------- /test-apps/custom-server/postcss.config.js: -------------------------------------------------------------------------------- 1 | import a from "postcss-import"; 2 | import b from "@tailwindcss/postcss"; 3 | 4 | export default { 5 | plugins: [ 6 | a, 7 | b, 8 | ], 9 | }; -------------------------------------------------------------------------------- /test-apps/custom-server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/test-apps/custom-server/public/favicon.ico -------------------------------------------------------------------------------- /test-apps/custom-server/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /test-apps/custom-server/server.js: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import morgan from "morgan"; 4 | 5 | // Short-circuit the type-checking of the built output. 6 | const BUILD_PATH = "./build/server/index.js"; 7 | const DEVELOPMENT = process.env.NODE_ENV === "development"; 8 | const PORT = Number.parseInt(process.env.PORT || "3000"); 9 | 10 | const app = express(); 11 | 12 | app.use(compression()); 13 | app.disable("x-powered-by"); 14 | 15 | if (DEVELOPMENT) { 16 | console.log("Starting development server"); 17 | const viteDevServer = await import("vite").then((vite) => 18 | vite.createServer({ 19 | server: { middlewareMode: true }, 20 | }), 21 | ); 22 | app.use(viteDevServer.middlewares); 23 | app.use(async (req, res, next) => { 24 | try { 25 | const source = await viteDevServer.ssrLoadModule("./server/app.ts"); 26 | return await source.app(req, res, next); 27 | } catch (error) { 28 | if (typeof error === "object" && error instanceof Error) { 29 | viteDevServer.ssrFixStacktrace(error); 30 | } 31 | next(error); 32 | } 33 | }); 34 | } else { 35 | console.log("Starting production server"); 36 | app.use( 37 | "/assets", 38 | express.static("build/client/assets", { immutable: true, maxAge: "1y" }), 39 | ); 40 | app.use(morgan("tiny")); 41 | app.use(express.static("build/client", { maxAge: "1h" })); 42 | app.use(await import(BUILD_PATH).then((mod) => mod.app)); 43 | } 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Server is running on http://localhost:${PORT}`); 47 | }); 48 | -------------------------------------------------------------------------------- /test-apps/custom-server/server/app.ts: -------------------------------------------------------------------------------- 1 | import "react-router"; 2 | import { createRequestHandler } from "@react-router/express"; 3 | import express from "express"; 4 | 5 | declare module "react-router" { 6 | interface AppLoadContext { 7 | VALUE_FROM_EXPRESS: string; 8 | } 9 | } 10 | 11 | export const app = express(); 12 | 13 | app.use( 14 | createRequestHandler({ 15 | build: () => import("virtual:react-router/server-build"), 16 | getLoadContext() { 17 | return { 18 | VALUE_FROM_EXPRESS: "Hello from Express", 19 | }; 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /test-apps/custom-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.vite.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-apps/custom-server/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["server.js", "vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-apps/custom-server/tsconfig.vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | ".react-router/types/**/*", 5 | "app/**/*", 6 | "app/**/.server/**/*", 7 | "app/**/.client/**/*", 8 | "server/**/*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "strict": true, 13 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 14 | "types": ["vite/client"], 15 | "target": "ES2022", 16 | "module": "ES2022", 17 | "moduleResolution": "bundler", 18 | "jsx": "react-jsx", 19 | "baseUrl": ".", 20 | "rootDirs": [".", "./.react-router/types"], 21 | "paths": { 22 | "~/*": ["./app/*"] 23 | }, 24 | "esModuleInterop": true, 25 | "resolveJsonModule": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test-apps/custom-server/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | import { reactRouterDevTools }from "react-router-devtools" 6 | export default defineConfig(({ isSsrBuild }) => ({ 7 | build: { 8 | rollupOptions: isSsrBuild 9 | ? { 10 | input: "./server/app.ts", 11 | } 12 | : undefined, 13 | }, 14 | plugins: [reactRouterDevTools(),tailwindcss(), reactRouter(), tsconfigPaths()], 15 | })); 16 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/README.md: -------------------------------------------------------------------------------- 1 | # templates/unstable-vite 2 | 3 | ⚠️ Remix support for Vite is unstable and not recommended for production. 4 | 5 | 📖 See the [Remix Vite docs][remix-vite-docs] for details on supported features. 6 | 7 | ## Setup 8 | 9 | ```shellscript 10 | npx create-remix@latest --template remix-run/remix/templates/unstable-vite 11 | ``` 12 | 13 | ## Run 14 | 15 | Spin up the Vite dev server: 16 | 17 | ```shellscript 18 | npm run dev 19 | ``` 20 | 21 | Or build your app for production and run it: 22 | 23 | ```shellscript 24 | npm run build 25 | npm run start 26 | ``` 27 | 28 | [remix-vite-docs]: https://remix.run/docs/en/main/future/vite 29 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/actions/home.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return { 3 | message: "Hello, world!", 4 | }; 5 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface ButtonProps extends React.ButtonHTMLAttributes { 4 | children: ReactNode 5 | } 6 | 7 | const Button = ({ children, ...props }: ButtonProps) => { 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | export { Button }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/modules/user.server.ts: -------------------------------------------------------------------------------- 1 | export const userSomething = () => { 2 | return "userSomething"; 3 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes.ts: -------------------------------------------------------------------------------- 1 | 2 | import { flatRoutes } from "@react-router/fs-routes"; 3 | import { type RouteConfig, index, layout, prefix, route } from "@react-router/dev/routes" 4 | import subroutes from "./subroutes.js" 5 | 6 | 7 | 8 | export default [...await flatRoutes(), ...subroutes ] -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.added.tsx: -------------------------------------------------------------------------------- 1 | export default function RouteComponent(){ 2 | return ( 3 |
4 | ); 5 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.final_test.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router"; 3 | import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router"; 4 | 5 | export const links: LinksFunction = () => ( 6 | [ 7 | // your links here 8 | ] 9 | ); 10 | 11 | export const meta: MetaFunction = () => [ 12 | // your meta here 13 | ]; 14 | 15 | export const handle = () => ({ 16 | // your handler here 17 | }); 18 | 19 | export const headers: HeadersFunction = () => ( 20 | { 21 | // your headers here 22 | } 23 | ); 24 | 25 | export const loader = async ({ request }: LoaderFunctionArgs) => { 26 | return null; 27 | }; 28 | 29 | export const action = async ({ request }: ActionFunctionArgs) => { 30 | return null; 31 | }; 32 | 33 | export default function RouteComponent(){ 34 | const data = useLoaderData() 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | export function ErrorBoundary(){ 41 | const error = useRouteError(); 42 | if (isRouteErrorResponse(error)) { 43 | return
44 | } 45 | return
46 | } 47 | 48 | export const shouldRevalidate: ShouldRevalidateFunction = () => { 49 | return true; 50 | }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.new_route.tsx: -------------------------------------------------------------------------------- 1 | export default function RouteComponent(){ 2 | return ( 3 |
4 | ); 5 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.one_more_time.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router"; 3 | import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router"; 4 | 5 | export const links: LinksFunction = () => ( 6 | [ 7 | // your links here 8 | ] 9 | ); 10 | 11 | export const meta: MetaFunction = () => [ 12 | // your meta here 13 | ]; 14 | 15 | export const handle = () => ({ 16 | // your handler here 17 | }); 18 | 19 | export const headers: HeadersFunction = () => ( 20 | { 21 | // your headers here 22 | } 23 | ); 24 | 25 | export const loader = async ({ request }: LoaderFunctionArgs) => { 26 | return null; 27 | }; 28 | 29 | export const action = async ({ request }: ActionFunctionArgs) => { 30 | return null; 31 | }; 32 | 33 | export default function RouteComponent(){ 34 | const data = useLoaderData() 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | export function ErrorBoundary(){ 41 | const error = useRouteError(); 42 | if (isRouteErrorResponse(error)) { 43 | return
44 | } 45 | return
46 | } 47 | 48 | export const shouldRevalidate: ShouldRevalidateFunction = () => { 49 | return true; 50 | }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.new.$test.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ActionFunctionArgs, data, Link, LoaderFunctionArgs, MetaFunction, Outlet, useFetcher, useLoaderData, useSubmit } from "react-router"; 3 | 4 | export const meta: MetaFunction = () => { 5 | return [ 6 | { title: "New Remix App" }, 7 | { name: "description", content: "Welcome to Remix!" }, 8 | ]; 9 | }; 10 | 11 | export const loader = async ({ request }: LoaderFunctionArgs) => { 12 | return data({ 13 | should: "work", 14 | with: { 15 | nested: { 16 | objects: { 17 | 18 | }, 19 | }, 20 | }, 21 | }, { headers: { "Cache-Control": "max-age=3600, private" } }); 22 | }; 23 | 24 | export const action = async ({ request }: ActionFunctionArgs) => { 25 | return new Response(JSON.stringify({ test: "died" })); 26 | }; 27 | 28 | export default function IndexRoute() { 29 | const string = useLoaderData(); 30 | const lFetcher = useFetcher(); 31 | const lFetcher2 = useFetcher(); 32 | const pFetcher = useFetcher(); 33 | const submit = useSubmit(); 34 | const data = new FormData(); 35 | data.append("test", "test"); 36 | return ( 37 |
38 |

Welcome to Remix 4

39 |
40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.tests.$id.edit.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | ActionFunctionArgs, 4 | Link, 5 | LoaderFunctionArgs, 6 | MetaFunction, 7 | Outlet, 8 | useFetcher, 9 | useLoaderData, 10 | useSubmit, 11 | } from "react-router"; 12 | 13 | export const meta: MetaFunction = () => { 14 | return [ 15 | { title: "New Remix App" }, 16 | { name: "description", content: "Welcome to Remix!" }, 17 | ]; 18 | }; 19 | 20 | export const loader = ({ request }: LoaderFunctionArgs) => { 21 | return { test: "died" } 22 | }; 23 | 24 | export const action = async ({ request }: ActionFunctionArgs) => { 25 | return ({ 26 | test: "died", 27 | }); 28 | }; 29 | 30 | export default function Index() { 31 | const data = new FormData(); 32 | data.append("test", "test"); 33 | return ( 34 |
35 |

36 |

37 | 38 |

39 |

40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.tests.$id.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | ActionFunctionArgs, 4 | json, 5 | Link, 6 | LoaderFunctionArgs, 7 | MetaFunction, 8 | Outlet, 9 | useFetcher, 10 | useLoaderData, 11 | useSubmit, 12 | } from "react-router"; 13 | 14 | export const meta: MetaFunction = () => { 15 | return [ 16 | { title: "New Remix App" }, 17 | { name: "description", content: "Welcome to Remix!" }, 18 | ]; 19 | }; 20 | 21 | export const loader = async ({ request }: LoaderFunctionArgs) => { 22 | return new Response( 23 | JSON.stringify({ 24 | test: "JSON stringify with new response and a long ass string to test something", 25 | }) 26 | ); 27 | }; 28 | 29 | export const action = async ({ request }: ActionFunctionArgs) => { 30 | return ({ 31 | test: "died", 32 | }); 33 | }; 34 | 35 | export default function Index() { 36 | const { message } = useLoaderData(); 37 | const lFetcher = useFetcher(); 38 | const pFetcher = useFetcher(); 39 | const submit = useSubmit(); 40 | const data = new FormData(); 41 | data.append("test", "test"); 42 | return ( 43 |
44 |

Welcome to Remix 2

45 | 46 | 47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.tests.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | ActionFunctionArgs, 4 | Link, 5 | LoaderFunctionArgs, 6 | MetaFunction, 7 | Outlet, 8 | useFetcher, 9 | useLoaderData, 10 | useSubmit, 11 | } from "react-router"; 12 | 13 | export const meta: MetaFunction = () => { 14 | return [ 15 | { title: "New Remix App" }, 16 | { name: "description", content: "Welcome to Remix!" }, 17 | ]; 18 | }; 19 | 20 | export const loader = async ({ request }: LoaderFunctionArgs) => { 21 | return new Response(new URLSearchParams("test=1&test=2&test=3")); 22 | }; 23 | 24 | export const action = async ({ request }: ActionFunctionArgs) => { 25 | return ({ 26 | test: "died", 27 | }); 28 | }; 29 | 30 | export default function Index() { 31 | const { message } = useLoaderData(); 32 | const lFetcher = useFetcher(); 33 | const pFetcher = useFetcher(); 34 | const submit = useSubmit(); 35 | const data = new FormData(); 36 | data.append("test", "test"); 37 | return ( 38 |
39 |

Welcome to Remix

40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoaderFunctionArgs, Outlet } from "react-router"; 3 | 4 | export const loader = async ({ request }: LoaderFunctionArgs) => { 5 | return { test: "returning raw object" }; 6 | }; 7 | 8 | export default function App() { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/_layout/test.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoaderFunctionArgs, useLoaderData } from "react-router"; 3 | 4 | export const loader = async ({ request }: LoaderFunctionArgs) => { 5 | return null; 6 | }; 7 | 8 | export default function RouteComponent(){ 9 | const data = useLoaderData() 10 | return ( 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/correct.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router"; 3 | import type { ActionFunctionArgs, HeadersFunction, LinksFunction, LoaderFunctionArgs, MetaFunction, ShouldRevalidateFunction } from "react-router"; 4 | 5 | export const links: LinksFunction = () => ( 6 | [ 7 | // your links here 8 | ] 9 | ); 10 | 11 | export const meta: MetaFunction = () => [ 12 | // your meta here 13 | ]; 14 | 15 | export const handle = () => ({ 16 | // your handler here 17 | }); 18 | 19 | export const headers: HeadersFunction = () => ( 20 | { 21 | // your headers here 22 | } 23 | ); 24 | 25 | export const loader = async ({ request }: LoaderFunctionArgs) => { 26 | return null; 27 | }; 28 | 29 | export const action = async ({ request }: ActionFunctionArgs) => { 30 | return null; 31 | }; 32 | 33 | export default function RouteComponent(){ 34 | const data = useLoaderData() 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | export function ErrorBoundary(){ 41 | const error = useRouteError(); 42 | if (isRouteErrorResponse(error)) { 43 | return
44 | } 45 | return
46 | } 47 | 48 | export const shouldRevalidate: ShouldRevalidateFunction = () => { 49 | return true; 50 | }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/dashboard.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLoaderData, Form, LoaderFunctionArgs } from 'react-router'; 3 | 4 | export const loader = async ({ request }: LoaderFunctionArgs) => { 5 | 6 | return { message: 'You are logged in!' }; 7 | }; 8 | 9 | export default function DashboardRoute() { 10 | const { message } = useLoaderData(); 11 | return ( 12 |
13 |

{message}

14 |
15 | 16 |
17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/epic-test+.tsx: -------------------------------------------------------------------------------- 1 | export default function Index() { 2 | console.log("Epic test") 3 | return
4 | Epic test 5 |
6 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/epic/__note-editor.server.tsx: -------------------------------------------------------------------------------- 1 | export const action = () => {} -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/epic/__note-editor.tsx: -------------------------------------------------------------------------------- 1 | export const NoteEditor = () =>{ return
} -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/epic/route.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { NoteEditor } from './__note-editor' 3 | 4 | export { action } from './__note-editor.server' 5 | 6 | export async function loader({ request }: any) { 7 | return {} 8 | } 9 | 10 | export default NoteEditor 11 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/exports.tsx: -------------------------------------------------------------------------------- 1 | export { loader, default }from "./_index"; 2 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/file.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | export default function File() { 4 | return ( 5 |
6 |

File Route

7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/folder/route.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | export default function Folder() { 4 | return
5 |

Folder Route

6 | 7 |
8 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLoaderData } from "react-router"; 3 | import { loader } from "../actions/home"; 4 | export { loader }; 5 | 6 | export function meta( ) { 7 | return [ 8 | { title: "New React Router App" }, 9 | { name: "description", content: "Welcome to React Router!" }, 10 | ]; 11 | } 12 | 13 | 14 | 15 | export default function Home() { 16 | const { message } = useLoaderData(); 17 | return
; 18 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Link } from "react-router"; 2 | 3 | interface SocialButtonProps { 4 | label: string; 5 | } 6 | 7 | 8 | export default function LoginRoute() { 9 | return ( 10 | <> 11 | Login 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/logout.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from "react-router"; 2 | 3 | 4 | 5 | export const action = async ({ request }: LoaderFunctionArgs) => { 6 | return null; 7 | }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/other._index.tsx: -------------------------------------------------------------------------------- 1 | export default function OtherIndexRoute() { 2 | return
Other Home
3 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/other.page.tsx: -------------------------------------------------------------------------------- 1 | export default function OtherPageRoute() { 2 | return
Other Page
3 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/other.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | export default function OtherLayout() { 4 | return
5 |

Other Layout

6 | 7 |
8 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/server-timings.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { ActionFunctionArgs, data, json, LoaderFunctionArgs, MetaFunction, redirect } from "react-router"; 4 | import { getServerTiming } from "~/timing.server"; 5 | 6 | 7 | export const meta: MetaFunction = () => { 8 | return [ 9 | { title: "New Remix App" }, 10 | { name: "description", content: "Welcome to Remix!" }, 11 | ]; 12 | }; 13 | 14 | 15 | export const loader = async ({ request, }: LoaderFunctionArgs) => { 16 | const { time, getServerTimingHeader } = getServerTiming(); 17 | await time("test", () => { 18 | return new Promise((resolve, reject) => { 19 | setTimeout(() => { 20 | resolve("test"); 21 | }, 300); 22 | }); 23 | }) 24 | await time("test1", () =>new Promise((resolve, reject) => { 25 | setTimeout(() => { 26 | resolve("test"); 27 | }, 200); 28 | })) 29 | return data({ message: "Hello World!", }, { 30 | headers: getServerTimingHeader(), 31 | }); 32 | }; 33 | 34 | export const action = async ({ request }: ActionFunctionArgs) => { 35 | return redirect("/login"); 36 | }; 37 | 38 | export default function Index() { 39 | 40 | return ( 41 |

42 | Server timings test route 43 |

44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/tester.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs, ClientLoaderFunctionArgs, ClientActionFunctionArgs, ActionFunctionArgs } from "react-router"; 2 | import { useLoaderData, isRouteErrorResponse, useRouteError } from "react-router"; 3 | 4 | export const loader = async ({ request }: LoaderFunctionArgs) => { 5 | return null; 6 | }; 7 | 8 | export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => { 9 | return null; 10 | }; 11 | 12 | export const action = async ({ request }: ActionFunctionArgs) => { 13 | return null; 14 | }; 15 | 16 | export const clientAction = async ({ request }: ClientActionFunctionArgs) => { 17 | return null; 18 | }; 19 | 20 | export default function RouteComponent(){ 21 | const data = useLoaderData() 22 | return ( 23 |
24 | ); 25 | } 26 | 27 | export function ErrorBoundary(){ 28 | const error = useRouteError(); 29 | if (isRouteErrorResponse(error)) { 30 | return
31 | } 32 | return
33 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/routes/unexported.tsx: -------------------------------------------------------------------------------- 1 | 2 | function loader() { 3 | return { name: 'React Router' }; 4 | } 5 | 6 | function Home({ loaderData }: Route.ComponentProps) { 7 | return ( 8 |
9 |

Hello, {loaderData.name}

10 | 14 | React Router Docs 15 | 16 |
17 | ); 18 | } 19 | 20 | export { loader, Home as default }; 21 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/subroutes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index, layout, prefix, route } from "@react-router/dev/routes" 2 | 3 | 4 | export default [ 5 | route("outside", "./routes/_index.tsx", { id: "something" }), 6 | 7 | ] satisfies RouteConfig 8 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/app/utils/example.ts: -------------------------------------------------------------------------------- 1 | // new file - app/utils/example.ts 2 | export const loader = async () => { 3 | return { data: "example" }; 4 | }; -------------------------------------------------------------------------------- /test-apps/react-router-vite/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-vite", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "react-router build", 8 | "dev": "react-router dev", 9 | "start": "react-router-serve ./build/server/index.js", 10 | "typecheck": "tsc" 11 | }, 12 | "dependencies": { 13 | "react-router": "7.1.4", 14 | "isbot": "^5.1.22", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "@react-router/dev": "7.1.4", 18 | "@react-router/node": "7.1.4", 19 | "@react-router/serve": "7.1.4" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^19.0.8", 23 | "@types/react-dom": "^19.0.3", 24 | "@react-router/fs-routes": "7.1.4", 25 | "eslint": "^9.19.0", 26 | "typescript": "^5.7.3", 27 | "vite": "^6.0.11", 28 | "vite-tsconfig-paths": "^5.1.4", 29 | "react-router-devtools": "*", 30 | "vite-plugin-inspect": "^10.1.0" 31 | }, 32 | "engines": { 33 | "node": ">=20.0.0" 34 | } 35 | } -------------------------------------------------------------------------------- /test-apps/react-router-vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-router-devtools/142df31f463bdcda783784cb7556aa82ef104054/test-apps/react-router-vite/public/favicon.ico -------------------------------------------------------------------------------- /test-apps/react-router-vite/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["../src/**/*.{tsx,ts}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "module": "ESNext", 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "target": "ES2022", 12 | "strict": true, 13 | "allowJs": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "~/*": ["./app/*"] 18 | }, 19 | 20 | // Remix takes care of building everything in `remix build`. 21 | "noEmit": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-apps/react-router-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import { defineConfig } from "vite"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | import { reactRouterDevTools, defineRdtConfig } from "react-router-devtools" 5 | 6 | import inspect from "vite-plugin-inspect" 7 | const config = defineRdtConfig({ 8 | client: { 9 | enableInspector: true, 10 | defaultOpen: false, 11 | position: "top-right", 12 | requireUrlFlag: false, 13 | liveUrls: [ 14 | { url: "https://forge42.dev", name: "Production" }, 15 | { 16 | url: "https://forge42.dev/staging", 17 | name: "Staging", 18 | }], 19 | }, 20 | pluginDir: "./plugins", 21 | includeInProd: { 22 | client: true, 23 | server: true 24 | }, 25 | // Set this option to true to suppress deprecation warnings 26 | // suppressDeprecationWarning: true, 27 | server: { 28 | serverTimingThreshold: 250, 29 | } 30 | }); 31 | 32 | export default defineConfig({ 33 | plugins: [ 34 | inspect(), 35 | reactRouterDevTools( config), 36 | reactRouter(), 37 | tsconfigPaths() 38 | ], 39 | optimizeDeps: { 40 | noDiscovery: true 41 | }, 42 | server: { 43 | open: true, 44 | port: 3000, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /test/console.ts: -------------------------------------------------------------------------------- 1 | vi.spyOn(console, "log").mockImplementation(() => undefined) 2 | vi.spyOn(console, "error").mockImplementation(() => undefined) 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Language and Environment */ 4 | "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 10 | "jsx": "react-jsx", 11 | "module": "ESNext" /* Specify what module code is generated. */, 12 | "rootDir": "." /* Specify the root folder within your source files. */, 13 | "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, 14 | "types": ["vitest/globals", "vite/client"], 15 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 16 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 17 | // "removeComments": true, /* Disable emitting comments. */ 18 | "noEmit": true /* Disable emitting files from a compilation. */, 19 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 20 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 21 | "forceConsistentCasingInFileNames": true, 22 | "strict": true, 23 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 24 | }, 25 | 26 | "exclude": [ 27 | "./bin/**/*.test.ts", 28 | "./dist/**/*", 29 | "./bin/mocks/**/*", 30 | "./test-apps/**/*", 31 | "./plugins/**/*", 32 | "./src/external/**/*", 33 | "./scripts", 34 | "./docs" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsup-client.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/client.ts"], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: false, 8 | dts: true, 9 | format: ["esm"], 10 | external: ["react"], 11 | }) 12 | -------------------------------------------------------------------------------- /tsup-context.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/context.ts"], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: false, 8 | dts: true, 9 | format: ["esm"], 10 | }) 11 | -------------------------------------------------------------------------------- /tsup-server.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/server.ts"], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: false, 8 | dts: true, 9 | format: ["esm"], 10 | }) 11 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: false, 8 | dts: true, 9 | format: ["esm"], 10 | external: ["vite-node"], 11 | }) 12 | --------------------------------------------------------------------------------